fix(animations): ensure web-animations are caught within the Angular zone

Closes #11881
Closes #11712
Closes #12355
Closes #11881
Closes #12546
Closes #12707
Closes #12774
This commit is contained in:
Matias Niemelä 2016-10-31 14:26:41 -07:00 committed by Victor Berchet
parent 6e35d13fbc
commit f80a157b65
5 changed files with 131 additions and 4 deletions

View File

@ -8,11 +8,18 @@
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {DomElementSchemaRegistry, ElementSchemaRegistry} from '@angular/compiler'; import {DomElementSchemaRegistry, ElementSchemaRegistry} from '@angular/compiler';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {WebAnimationsDriver} from '@angular/platform-browser/src/dom/web_animations_driver';
import {WebAnimationsPlayer} from '@angular/platform-browser/src/dom/web_animations_player';
import {expect} from '@angular/platform-browser/testing/matchers';
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
import {Component} from '../../index'; import {DomAnimatePlayer} from '../../../platform-browser/src/dom/dom_animate_player';
import {ApplicationRef, Component, HostBinding, HostListener, NgModule, NgZone, destroyPlatform} from '../../index';
import {DEFAULT_STATE} from '../../src/animation/animation_constants'; import {DEFAULT_STATE} from '../../src/animation/animation_constants';
import {AnimationGroupPlayer} from '../../src/animation/animation_group_player'; import {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
import {AnimationKeyframe} from '../../src/animation/animation_keyframe'; import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
@ -2068,6 +2075,57 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
}); });
}); });
describe('full animation integration tests', () => {
if (!getDOM().supportsWebAnimation()) return;
var el: any, testProviders: any[];
beforeEach(() => {
destroyPlatform();
let fakeDoc = getDOM().createHtmlDocument();
el = getDOM().createElement('animation-app', fakeDoc);
getDOM().appendChild(fakeDoc.body, el);
testProviders = [
{provide: DOCUMENT, useValue: fakeDoc},
{provide: AnimationDriver, useClass: ExtendedWebAnimationsDriver}
];
});
afterEach(() => { destroyPlatform(); });
it('should automatically run change detection when the animation done callback code updates any bindings',
(asyncDone: Function) => {
bootstrap(AnimationAppCmp, testProviders).then(ref => {
let appRef = <ApplicationRef>ref.injector.get(ApplicationRef);
let appCmp: AnimationAppCmp =
appRef.components.find(cmp => cmp.componentType === AnimationAppCmp).instance;
let driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
let zone: NgZone = ref.injector.get(NgZone);
let text = '';
zone.run(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: pending/);
expect(text).toMatch(/Animation Time: 0/);
appCmp.animationStatus = 'on';
setTimeout(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: started/);
expect(text).toMatch(/Animation Time: 555/);
var player = driver.players.pop().domPlayer;
getDOM().dispatchEvent(player, getDOM().createEvent('finish'));
setTimeout(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: done/);
expect(text).toMatch(/Animation Time: 555/);
asyncDone();
}, 0);
}, 0);
});
});
});
});
} }
class InnerContentTrackingAnimationDriver extends MockAnimationDriver { class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
@ -2150,3 +2208,60 @@ class _NaiveElementSchema extends DomElementSchemaRegistry {
return {error: null, value: <string>val}; return {error: null, value: <string>val};
} }
} }
@Component({
selector: 'animation-app',
animations: [trigger('animationStatus', [transition('off => on', animate(555))])],
template: `
Animation Time: {{ time }}
Animation Status: {{ status }}
`
})
class AnimationAppCmp {
time: number = 0;
status: string = 'pending';
@HostBinding('@animationStatus')
animationStatus = 'off';
@HostListener('@animationStatus.start', ['$event'])
onStart(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'started';
}
}
@HostListener('@animationStatus.done', ['$event'])
onDone(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'done';
}
}
}
class AnimationTestModule {}
function bootstrap(cmpType: any, providers: any[]): Promise<any> {
@NgModule({
imports: [BrowserModule],
providers: providers,
declarations: [cmpType],
bootstrap: [cmpType]
})
class AnimationTestModule {
}
return platformBrowserDynamic().bootstrapModule(AnimationTestModule);
}
class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
players: WebAnimationsPlayer[] = [];
animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): WebAnimationsPlayer {
var player = super.animate(element, startingStyles, keyframes, duration, delay, easing);
this.players.push(player);
return player;
}
}

View File

@ -14,4 +14,6 @@ export interface DomAnimatePlayer {
onfinish: Function; onfinish: Function;
position: number; position: number;
currentTime: number; currentTime: number;
addEventListener(eventName: string, handler: (event: any) => any): any;
dispatchEvent(eventName: string): any;
} }

View File

@ -55,7 +55,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
// this is required so that the player doesn't start to animate right away // this is required so that the player doesn't start to animate right away
this._resetDomPlayerState(); this._resetDomPlayerState();
this._player.onfinish = () => this._onFinish(); this._player.addEventListener('finish', () => this._onFinish());
} }
/** @internal */ /** @internal */
@ -63,6 +63,8 @@ export class WebAnimationsPlayer implements AnimationPlayer {
return <DomAnimatePlayer>element.animate(keyframes, options); return <DomAnimatePlayer>element.animate(keyframes, options);
} }
get domPlayer() { return this._player; }
onStart(fn: () => void): void { this._onStartFns.push(fn); } onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); } onDone(fn: () => void): void { this._onDoneFns.push(fn); }

View File

@ -14,7 +14,7 @@ import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer { class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
public domPlayer = new MockDomAnimatePlayer(); private _overriddenDomPlayer = new MockDomAnimatePlayer();
constructor( constructor(
public element: HTMLElement, public keyframes: {[key: string]: string | number}[], public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
@ -22,9 +22,11 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
super(element, keyframes, options); super(element, keyframes, options);
} }
get domPlayer() { return this._overriddenDomPlayer; }
/** @internal */ /** @internal */
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
return this.domPlayer; return this._overriddenDomPlayer;
} }
} }

View File

@ -40,4 +40,10 @@ export class MockDomAnimatePlayer implements DomAnimatePlayer {
this._position = val; this._position = val;
} }
get position(): number { return this._position; } get position(): number { return this._position; }
addEventListener(eventName: string, handler: (event: any) => any): any {
if (eventName == 'finish') {
this.onfinish = handler;
}
}
dispatchEvent(eventName: string): any {}
} }