From f80a157b656a5fe464ed9d3f257d19b74e721af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 31 Oct 2016 14:26:41 -0700 Subject: [PATCH] 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 --- .../animation/animation_integration_spec.ts | 117 +++++++++++++++++- .../src/dom/dom_animate_player.ts | 2 + .../src/dom/web_animations_player.ts | 4 +- .../test/dom/web_animations_player_spec.ts | 6 +- .../testing/mock_dom_animate_player.ts | 6 + 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index de20f9a027..702d7fa85a 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -8,11 +8,18 @@ import {CommonModule} from '@angular/common'; 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 {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 {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 {AnimationGroupPlayer} from '../../src/animation/animation_group_player'; 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 = 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 { @@ -2150,3 +2208,60 @@ class _NaiveElementSchema extends DomElementSchemaRegistry { return {error: null, value: 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 { + @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; + } +} diff --git a/modules/@angular/platform-browser/src/dom/dom_animate_player.ts b/modules/@angular/platform-browser/src/dom/dom_animate_player.ts index b82cbd8fa6..aeaaf8adc3 100644 --- a/modules/@angular/platform-browser/src/dom/dom_animate_player.ts +++ b/modules/@angular/platform-browser/src/dom/dom_animate_player.ts @@ -14,4 +14,6 @@ export interface DomAnimatePlayer { onfinish: Function; position: number; currentTime: number; + addEventListener(eventName: string, handler: (event: any) => any): any; + dispatchEvent(eventName: string): any; } diff --git a/modules/@angular/platform-browser/src/dom/web_animations_player.ts b/modules/@angular/platform-browser/src/dom/web_animations_player.ts index a89c050185..100cde30de 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_player.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_player.ts @@ -55,7 +55,7 @@ export class WebAnimationsPlayer implements AnimationPlayer { // this is required so that the player doesn't start to animate right away this._resetDomPlayerState(); - this._player.onfinish = () => this._onFinish(); + this._player.addEventListener('finish', () => this._onFinish()); } /** @internal */ @@ -63,6 +63,8 @@ export class WebAnimationsPlayer implements AnimationPlayer { return element.animate(keyframes, options); } + get domPlayer() { return this._player; } + onStart(fn: () => void): void { this._onStartFns.push(fn); } onDone(fn: () => void): void { this._onDoneFns.push(fn); } diff --git a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts index 1ef487e25c..f032202c19 100644 --- a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts +++ b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts @@ -14,7 +14,7 @@ import {WebAnimationsPlayer} from '../../src/dom/web_animations_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer { - public domPlayer = new MockDomAnimatePlayer(); + private _overriddenDomPlayer = new MockDomAnimatePlayer(); constructor( public element: HTMLElement, public keyframes: {[key: string]: string | number}[], @@ -22,9 +22,11 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer { super(element, keyframes, options); } + get domPlayer() { return this._overriddenDomPlayer; } + /** @internal */ _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { - return this.domPlayer; + return this._overriddenDomPlayer; } } diff --git a/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts b/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts index 2f77ba1ae7..9d7b534a91 100644 --- a/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts +++ b/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts @@ -40,4 +40,10 @@ export class MockDomAnimatePlayer implements DomAnimatePlayer { this._position = val; } get position(): number { return this._position; } + addEventListener(eventName: string, handler: (event: any) => any): any { + if (eventName == 'finish') { + this.onfinish = handler; + } + } + dispatchEvent(eventName: string): any {} }