perf(animations): always run the animation queue outside of zones
Related #12732 Closes #13440
This commit is contained in:

committed by
Victor Berchet

parent
ecfad467a1
commit
e2622add07
@ -1493,6 +1493,36 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
expect(message).toMatch(/Couldn't find an animation entry for "something"/);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation output is referenced that is not bound to as a property on the same element',
|
||||
() => {
|
||||
TestBed.overrideComponent(DummyLoadingCmp, {
|
||||
set: {
|
||||
template: `
|
||||
<if-cmp (@trigger.done)="callback($event)"></if-cmp>
|
||||
`
|
||||
}
|
||||
});
|
||||
TestBed.overrideComponent(DummyIfCmp, {
|
||||
set: {
|
||||
template: `
|
||||
<div [@trigger]="exp"></div>
|
||||
`,
|
||||
animations: [trigger('trigger', [transition('one => two', [animate(1000)])])]
|
||||
}
|
||||
});
|
||||
|
||||
let message = '';
|
||||
try {
|
||||
const fixture = TestBed.createComponent(DummyIfCmp);
|
||||
fixture.detectChanges();
|
||||
} catch (e) {
|
||||
message = e.message;
|
||||
}
|
||||
|
||||
expect(message).toMatch(
|
||||
/Unable to listen on \(@trigger.done\) because the animation trigger \[@trigger\] isn't being used on the same element/);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation output is referenced that is not bound to as a property on the same element',
|
||||
() => {
|
||||
TestBed.overrideComponent(DummyIfCmp, {
|
||||
@ -2177,6 +2207,23 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
];
|
||||
});
|
||||
|
||||
function assertStatus(value: string) {
|
||||
const text = getDOM().getText(el);
|
||||
const regexp = new RegExp(`Animation Status: ${value}`);
|
||||
expect(text).toMatch(regexp);
|
||||
}
|
||||
|
||||
function assertTime(value: number) {
|
||||
const text = getDOM().getText(el);
|
||||
const regexp = new RegExp(`Animation Time: ${value}`);
|
||||
expect(text).toMatch(regexp);
|
||||
}
|
||||
|
||||
function finishAnimation(player: WebAnimationsPlayer, cb: () => any) {
|
||||
getDOM().dispatchEvent(player.domPlayer, getDOM().createEvent('finish'));
|
||||
Promise.resolve(null).then(cb);
|
||||
}
|
||||
|
||||
afterEach(() => { destroyPlatform(); });
|
||||
|
||||
it('should automatically run change detection when the animation done callback code updates any bindings',
|
||||
@ -2187,24 +2234,82 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
appRef.components.find(cmp => cmp.componentType === AnimationAppCmp).instance;
|
||||
const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
|
||||
const 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/);
|
||||
assertStatus('pending');
|
||||
assertTime(0);
|
||||
appCmp.animationStatus = 'on';
|
||||
setTimeout(() => {
|
||||
text = getDOM().getText(el);
|
||||
expect(text).toMatch(/Animation Status: started/);
|
||||
expect(text).toMatch(/Animation Time: 555/);
|
||||
const player = driver.players.pop().domPlayer;
|
||||
getDOM().dispatchEvent(player, getDOM().createEvent('finish'));
|
||||
zone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
text = getDOM().getText(el);
|
||||
expect(text).toMatch(/Animation Status: done/);
|
||||
expect(text).toMatch(/Animation Time: 555/);
|
||||
asyncDone();
|
||||
assertStatus('started');
|
||||
assertTime(555);
|
||||
const player = driver.players.pop();
|
||||
finishAnimation(player, () => {
|
||||
assertStatus('done');
|
||||
assertTime(555);
|
||||
asyncDone();
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not run change detection for an animation that contains multiple steps until a callback is fired',
|
||||
(asyncDone: Function) => {
|
||||
bootstrap(AnimationAppCmp, testProviders).then(ref => {
|
||||
const appRef = <ApplicationRef>ref.injector.get(ApplicationRef);
|
||||
const appCmpDetails: any =
|
||||
appRef.components.find(cmp => cmp.componentType === AnimationAppCmp);
|
||||
const appCD = appCmpDetails.changeDetectorRef;
|
||||
const appCmp: AnimationAppCmp = appCmpDetails.instance;
|
||||
const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
|
||||
const zone: NgZone = ref.injector.get(NgZone);
|
||||
|
||||
let player: WebAnimationsPlayer;
|
||||
let onDoneCalls: string[] = [];
|
||||
function onDoneFn(value: string) {
|
||||
return () => {
|
||||
NgZone.assertNotInAngularZone();
|
||||
onDoneCalls.push(value);
|
||||
appCmp.status = value;
|
||||
};
|
||||
};
|
||||
|
||||
zone.run(() => {
|
||||
assertStatus('pending');
|
||||
appCmp.animationWithSteps = 'on';
|
||||
|
||||
setTimeout(() => {
|
||||
expect(driver.players.length).toEqual(3);
|
||||
assertStatus('started');
|
||||
|
||||
zone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
assertStatus('started');
|
||||
player = driver.players.shift();
|
||||
player.onDone(onDoneFn('1'));
|
||||
|
||||
// step 1 => 2
|
||||
finishAnimation(player, () => {
|
||||
assertStatus('started');
|
||||
player = driver.players.shift();
|
||||
player.onDone(onDoneFn('2'));
|
||||
|
||||
// step 2 => 3
|
||||
finishAnimation(player, () => {
|
||||
assertStatus('started');
|
||||
player = driver.players.shift();
|
||||
player.onDone(onDoneFn('3'));
|
||||
|
||||
// step 3 => done
|
||||
finishAnimation(player, () => {
|
||||
assertStatus('done');
|
||||
asyncDone();
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
@ -2295,7 +2400,17 @@ class _NaiveElementSchema extends DomElementSchemaRegistry {
|
||||
|
||||
@Component({
|
||||
selector: 'animation-app',
|
||||
animations: [trigger('animationStatus', [transition('off => on', animate(555))])],
|
||||
animations: [
|
||||
trigger('animationStatus', [transition('off => on', animate(555))]),
|
||||
trigger(
|
||||
'animationWithSteps',
|
||||
[transition(
|
||||
'* => on',
|
||||
[
|
||||
style({height: '0px'}), animate(100, style({height: '100px'})),
|
||||
animate(100, style({height: '200px'})), animate(100, style({height: '300px'}))
|
||||
])])
|
||||
],
|
||||
template: `
|
||||
Animation Time: {{ time }}
|
||||
Animation Status: {{ status }}
|
||||
@ -2309,7 +2424,7 @@ class AnimationAppCmp {
|
||||
animationStatus = 'off';
|
||||
|
||||
@HostListener('@animationStatus.start', ['$event'])
|
||||
onStart(event: AnimationTransitionEvent) {
|
||||
onAnimationStartDone(event: AnimationTransitionEvent) {
|
||||
if (event.toState == 'on') {
|
||||
this.time = event.totalTime;
|
||||
this.status = 'started';
|
||||
@ -2317,7 +2432,26 @@ class AnimationAppCmp {
|
||||
}
|
||||
|
||||
@HostListener('@animationStatus.done', ['$event'])
|
||||
onDone(event: AnimationTransitionEvent) {
|
||||
onAnimationStatusDone(event: AnimationTransitionEvent) {
|
||||
if (event.toState == 'on') {
|
||||
this.time = event.totalTime;
|
||||
this.status = 'done';
|
||||
}
|
||||
}
|
||||
|
||||
@HostBinding('@animationWithSteps')
|
||||
animationWithSteps = 'off';
|
||||
|
||||
@HostListener('@animationWithSteps.start', ['$event'])
|
||||
onAnimationWithStepsStart(event: AnimationTransitionEvent) {
|
||||
if (event.toState == 'on') {
|
||||
this.time = event.totalTime;
|
||||
this.status = 'started';
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('@animationWithSteps.done', ['$event'])
|
||||
onAnimationWithStepsDone(event: AnimationTransitionEvent) {
|
||||
if (event.toState == 'on') {
|
||||
this.time = event.totalTime;
|
||||
this.status = 'done';
|
||||
|
143
modules/@angular/core/test/animation/animation_queue_spec.ts
Normal file
143
modules/@angular/core/test/animation/animation_queue_spec.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @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 {AnimationQueue} from '@angular/core/src/animation/animation_queue';
|
||||
|
||||
import {NgZone} from '../../src/zone/ng_zone';
|
||||
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/mock_animation_player';
|
||||
import {beforeEach, describe, expect, it} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
describe('AnimationQueue', function() {
|
||||
beforeEach(() => { TestBed.configureTestingModule({declarations: [], imports: []}); });
|
||||
|
||||
it('should queue animation players and run when flushed, but only as the next scheduled microtask',
|
||||
fakeAsync(() => {
|
||||
const zone = TestBed.get(NgZone);
|
||||
const queue = new AnimationQueue(zone);
|
||||
|
||||
const log: string[] = [];
|
||||
const p1 = new MockAnimationPlayer();
|
||||
const p2 = new MockAnimationPlayer();
|
||||
const p3 = new MockAnimationPlayer();
|
||||
|
||||
p1.onStart(() => log.push('1'));
|
||||
p2.onStart(() => log.push('2'));
|
||||
p3.onStart(() => log.push('3'));
|
||||
|
||||
queue.enqueue(p1);
|
||||
queue.enqueue(p2);
|
||||
queue.enqueue(p3);
|
||||
expect(log).toEqual([]);
|
||||
|
||||
queue.flush();
|
||||
expect(log).toEqual([]);
|
||||
|
||||
flushMicrotasks();
|
||||
expect(log).toEqual(['1', '2', '3']);
|
||||
}));
|
||||
|
||||
it('should always run each of the animation players outside of the angular zone on start',
|
||||
fakeAsync(() => {
|
||||
const zone = TestBed.get(NgZone);
|
||||
const queue = new AnimationQueue(zone);
|
||||
|
||||
const player = new MockAnimationPlayer();
|
||||
let eventHasRun = false;
|
||||
player.onStart(() => {
|
||||
NgZone.assertNotInAngularZone();
|
||||
eventHasRun = true;
|
||||
});
|
||||
|
||||
zone.run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
queue.enqueue(player);
|
||||
queue.flush();
|
||||
flushMicrotasks();
|
||||
});
|
||||
|
||||
expect(eventHasRun).toBe(true);
|
||||
}));
|
||||
|
||||
it('should always run each of the animation players outside of the angular zone on done',
|
||||
fakeAsync(() => {
|
||||
const zone = TestBed.get(NgZone);
|
||||
const queue = new AnimationQueue(zone);
|
||||
|
||||
const player = new MockAnimationPlayer();
|
||||
let eventHasRun = false;
|
||||
player.onDone(() => {
|
||||
NgZone.assertNotInAngularZone();
|
||||
eventHasRun = true;
|
||||
});
|
||||
|
||||
zone.run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
queue.enqueue(player);
|
||||
queue.flush();
|
||||
flushMicrotasks();
|
||||
});
|
||||
|
||||
expect(eventHasRun).toBe(false);
|
||||
player.finish();
|
||||
expect(eventHasRun).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not run animations again incase an animation midway fails', fakeAsync(() => {
|
||||
const zone = TestBed.get(NgZone);
|
||||
const queue = new AnimationQueue(zone);
|
||||
|
||||
const log: string[] = [];
|
||||
const p1 = new PlayerThatFails(false);
|
||||
const p2 = new PlayerThatFails(true);
|
||||
const p3 = new PlayerThatFails(false);
|
||||
|
||||
p1.onStart(() => log.push('1'));
|
||||
p2.onStart(() => log.push('2'));
|
||||
p3.onStart(() => log.push('3'));
|
||||
|
||||
queue.enqueue(p1);
|
||||
queue.enqueue(p2);
|
||||
queue.enqueue(p3);
|
||||
|
||||
queue.flush();
|
||||
|
||||
expect(() => flushMicrotasks()).toThrowError();
|
||||
|
||||
expect(log).toEqual(['1', '2']);
|
||||
|
||||
// let's reset this so that it gets triggered again
|
||||
p2.reset();
|
||||
p2.onStart(() => log.push('2'));
|
||||
|
||||
queue.flush();
|
||||
|
||||
expect(() => flushMicrotasks()).not.toThrowError();
|
||||
|
||||
expect(log).toEqual(['1', '2', '3']);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
class PlayerThatFails extends MockAnimationPlayer {
|
||||
private _animationStarted = false;
|
||||
|
||||
constructor(public doFail: boolean) { super(); }
|
||||
|
||||
play() {
|
||||
super.play();
|
||||
this._animationStarted = true;
|
||||
if (this.doFail) {
|
||||
throw new Error('Oh nooooo');
|
||||
}
|
||||
}
|
||||
|
||||
reset() { this._animationStarted = false; }
|
||||
|
||||
hasStarted() { return this._animationStarted; }
|
||||
}
|
@ -9,53 +9,54 @@
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {NoOpAnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {AnimationQueue} from '../../src/animation/animation_queue';
|
||||
import {AnimationViewContext} from '../../src/linker/animation_view_context';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {describe, expect, iit, it} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
describe('AnimationViewContext', function() {
|
||||
let viewContext: AnimationViewContext;
|
||||
let elm: any;
|
||||
beforeEach(() => {
|
||||
viewContext = new AnimationViewContext();
|
||||
elm = el('<div></div>');
|
||||
});
|
||||
beforeEach(() => { elm = el('<div></div>'); });
|
||||
|
||||
function getPlayers() { return viewContext.getAnimationPlayers(elm); }
|
||||
function getPlayers(vc: any) { return vc.getAnimationPlayers(elm); }
|
||||
|
||||
it('should remove the player from the registry once the animation is complete',
|
||||
fakeAsync(() => {
|
||||
const player = new NoOpAnimationPlayer();
|
||||
const animationQueue = TestBed.get(AnimationQueue) as AnimationQueue;
|
||||
const vc = new AnimationViewContext(animationQueue);
|
||||
|
||||
expect(getPlayers().length).toEqual(0);
|
||||
viewContext.queueAnimation(elm, 'someAnimation', player);
|
||||
expect(getPlayers().length).toEqual(1);
|
||||
expect(getPlayers(vc).length).toEqual(0);
|
||||
vc.queueAnimation(elm, 'someAnimation', player);
|
||||
expect(getPlayers(vc).length).toEqual(1);
|
||||
player.finish();
|
||||
expect(getPlayers().length).toEqual(0);
|
||||
expect(getPlayers(vc).length).toEqual(0);
|
||||
}));
|
||||
|
||||
it('should not remove a follow-up player from the registry if another player is queued',
|
||||
fakeAsync(() => {
|
||||
const player1 = new NoOpAnimationPlayer();
|
||||
const player2 = new NoOpAnimationPlayer();
|
||||
const animationQueue = TestBed.get(AnimationQueue) as AnimationQueue;
|
||||
const vc = new AnimationViewContext(animationQueue);
|
||||
|
||||
viewContext.queueAnimation(elm, 'someAnimation', player1);
|
||||
expect(getPlayers().length).toBe(1);
|
||||
expect(getPlayers()[0]).toBe(player1);
|
||||
vc.queueAnimation(elm, 'someAnimation', player1);
|
||||
expect(getPlayers(vc).length).toBe(1);
|
||||
expect(getPlayers(vc)[0]).toBe(player1);
|
||||
|
||||
viewContext.queueAnimation(elm, 'someAnimation', player2);
|
||||
expect(getPlayers().length).toBe(1);
|
||||
expect(getPlayers()[0]).toBe(player2);
|
||||
vc.queueAnimation(elm, 'someAnimation', player2);
|
||||
expect(getPlayers(vc).length).toBe(1);
|
||||
expect(getPlayers(vc)[0]).toBe(player2);
|
||||
|
||||
player1.finish();
|
||||
|
||||
expect(getPlayers().length).toBe(1);
|
||||
expect(getPlayers()[0]).toBe(player2);
|
||||
expect(getPlayers(vc).length).toBe(1);
|
||||
expect(getPlayers(vc)[0]).toBe(player2);
|
||||
|
||||
player2.finish();
|
||||
|
||||
expect(getPlayers().length).toBe(0);
|
||||
expect(getPlayers(vc).length).toBe(0);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user