fix(animations): only require one flushMicrotasks call when testing animations

This commit is contained in:
Matias Niemelä 2017-05-18 19:26:20 -07:00
parent eed67ddafb
commit 6cb93c1fac
6 changed files with 101 additions and 30 deletions

View File

@ -86,7 +86,7 @@ export class AnimationEngine {
return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback); return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback);
} }
flush(): void { this._transitionEngine.flush(); } flush(countId: number = -1): void { this._transitionEngine.flush(countId); }
get players(): AnimationPlayer[] { get players(): AnimationPlayer[] {
return (this._transitionEngine.players as AnimationPlayer[]) return (this._transitionEngine.players as AnimationPlayer[])

View File

@ -80,9 +80,14 @@ export function listenOnPlayer(
export function copyAnimationEvent( export function copyAnimationEvent(
e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent { e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent {
return makeAnimationEvent( const event = makeAnimationEvent(
e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName, e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName,
totalTime == undefined ? e.totalTime : totalTime); totalTime == undefined ? e.totalTime : totalTime);
const data = (e as any)['_data'];
if (data != null) {
(event as any)['_data'] = data;
}
return event;
} }
export function makeAnimationEvent( export function makeAnimationEvent(

View File

@ -382,7 +382,7 @@ export class AnimationTransitionNamespace {
insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); } insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); }
drainQueuedTransitions(): QueueInstruction[] { drainQueuedTransitions(countId: number): QueueInstruction[] {
const instructions: QueueInstruction[] = []; const instructions: QueueInstruction[] = [];
this._queue.forEach(entry => { this._queue.forEach(entry => {
const player = entry.player; const player = entry.player;
@ -395,6 +395,7 @@ export class AnimationTransitionNamespace {
if (listener.name == entry.triggerName) { if (listener.name == entry.triggerName) {
const baseEvent = makeAnimationEvent( const baseEvent = makeAnimationEvent(
element, entry.triggerName, entry.fromState.value, entry.toState.value); element, entry.triggerName, entry.fromState.value, entry.toState.value);
(baseEvent as any)['_data'] = countId;
listenOnPlayer(entry.player, listener.phase, baseEvent, listener.callback); listenOnPlayer(entry.player, listener.phase, baseEvent, listener.callback);
} }
}); });
@ -627,7 +628,7 @@ export class TransitionAnimationEngine {
}); });
} }
flush() { flush(countId: number = -1) {
let players: AnimationPlayer[] = []; let players: AnimationPlayer[] = [];
if (this.newHostElements.size) { if (this.newHostElements.size) {
this.newHostElements.forEach((ns, element) => { this._balanceNamespaceList(ns, element); }); this.newHostElements.forEach((ns, element) => { this._balanceNamespaceList(ns, element); });
@ -635,7 +636,7 @@ export class TransitionAnimationEngine {
} }
if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) { if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) {
players = this._flushAnimations(); players = this._flushAnimations(countId);
} }
this.totalQueuedPlayers = 0; this.totalQueuedPlayers = 0;
@ -659,7 +660,7 @@ export class TransitionAnimationEngine {
} }
} }
private _flushAnimations(): TransitionAnimationPlayer[] { private _flushAnimations(countId: number): TransitionAnimationPlayer[] {
const subTimelines = new ElementInstructionMap(); const subTimelines = new ElementInstructionMap();
const skippedPlayers: TransitionAnimationPlayer[] = []; const skippedPlayers: TransitionAnimationPlayer[] = [];
const skippedPlayersMap = new Map<any, AnimationPlayer[]>(); const skippedPlayersMap = new Map<any, AnimationPlayer[]>();
@ -677,8 +678,9 @@ export class TransitionAnimationEngine {
for (let i = this._namespaceList.length - 1; i >= 0; i--) { for (let i = this._namespaceList.length - 1; i >= 0; i--) {
const ns = this._namespaceList[i]; const ns = this._namespaceList[i];
ns.drainQueuedTransitions().forEach(entry => { ns.drainQueuedTransitions(countId).forEach(entry => {
const player = entry.player; const player = entry.player;
const element = entry.element; const element = entry.element;
if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { if (!bodyNode || !this.driver.containsElement(bodyNode, element)) {
player.destroy(); player.destroy();
@ -746,7 +748,7 @@ export class TransitionAnimationEngine {
} }
}); });
allPreviousPlayersMap.forEach(players => { players.forEach(player => player.destroy()); }); allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
const leaveNodes: any[] = bodyNode && allPostStyleElements.size ? const leaveNodes: any[] = bodyNode && allPostStyleElements.size ?
listToArray(this.driver.query(bodyNode, LEAVE_SELECTOR, true)) : listToArray(this.driver.query(bodyNode, LEAVE_SELECTOR, true)) :

View File

@ -292,6 +292,7 @@ export function main() {
setProperty(element, engine, 'myTrigger', '456'); setProperty(element, engine, 'myTrigger', '456');
engine.flush(); engine.flush();
delete (capture as any)['_data'];
expect(capture).toEqual({ expect(capture).toEqual({
element, element,
triggerName: 'myTrigger', triggerName: 'myTrigger',
@ -305,6 +306,7 @@ export function main() {
const player = engine.players.pop() !; const player = engine.players.pop() !;
player.finish(); player.finish();
delete (capture as any)['_data'];
expect(capture).toEqual({ expect(capture).toEqual({
element, element,
triggerName: 'myTrigger', triggerName: 'myTrigger',

View File

@ -37,6 +37,51 @@ export function main() {
}); });
}); });
describe('fakeAsync testing', () => {
it('should only require one flushMicrotasks call to kick off animation callbacks',
fakeAsync(() => {
@Component({
selector: 'cmp',
template: `
<div [@myAnimation]="exp" (@myAnimation.start)="cb('start')" (@myAnimation.done)="cb('done')"></div>
`,
animations: [trigger(
'myAnimation',
[transition('* => on, * => off', [animate(1000, style({opacity: 1}))])])]
})
class Cmp {
exp: any = false;
status: string = '';
cb(status: string) { this.status = status; }
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'on';
fixture.detectChanges();
expect(cmp.status).toEqual('');
flushMicrotasks();
expect(cmp.status).toEqual('start');
let player = MockAnimationDriver.log.pop() !;
player.finish();
expect(cmp.status).toEqual('done');
cmp.status = '';
cmp.exp = 'off';
fixture.detectChanges();
expect(cmp.status).toEqual('');
player = MockAnimationDriver.log.pop() !;
player.finish();
expect(cmp.status).toEqual('');
flushMicrotasks();
expect(cmp.status).toEqual('done');
}));
});
describe('component fixture integration', () => { describe('component fixture integration', () => {
describe('whenRenderingDone', () => { describe('whenRenderingDone', () => {
it('should wait until the animations are finished until continuing', fakeAsync(() => { it('should wait until the animations are finished until continuing', fakeAsync(() => {

View File

@ -12,6 +12,8 @@ import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, Re
@Injectable() @Injectable()
export class AnimationRendererFactory implements RendererFactory2 { export class AnimationRendererFactory implements RendererFactory2 {
private _currentId: number = 0; private _currentId: number = 0;
private _currentFlushId: number = 1;
private _animationCallbacksBuffer: [(e: any) => any, any][] = [];
constructor( constructor(
private delegate: RendererFactory2, private _engine: AnimationEngine, private _zone: NgZone) { private delegate: RendererFactory2, private _engine: AnimationEngine, private _zone: NgZone) {
@ -38,7 +40,7 @@ export class AnimationRendererFactory implements RendererFactory2 {
animationTriggers.forEach( animationTriggers.forEach(
trigger => this._engine.registerTrigger( trigger => this._engine.registerTrigger(
componentId, namespaceId, hostElement, trigger.name, trigger)); componentId, namespaceId, hostElement, trigger.name, trigger));
return new AnimationRenderer(delegate, this._engine, this._zone, namespaceId); return new AnimationRenderer(this, delegate, this._engine, this._zone, namespaceId);
} }
begin() { begin() {
@ -47,8 +49,38 @@ export class AnimationRendererFactory implements RendererFactory2 {
} }
} }
private _scheduleCountTask() {
Zone.current.scheduleMicroTask(
'incremenet the animation microtask', () => { this._currentFlushId++; });
}
/* @internal */
scheduleListenerCallback(count: number, fn: (e: any) => any, data: any) {
if (count >= 0 && count < this._currentFlushId) {
this._zone.run(() => fn(data));
return;
}
if (this._animationCallbacksBuffer.length == 0) {
Promise.resolve(null).then(() => {
this._zone.run(() => {
this._animationCallbacksBuffer.forEach(tuple => {
const [fn, data] = tuple;
fn(data);
});
this._animationCallbacksBuffer = [];
});
});
}
this._animationCallbacksBuffer.push([fn, data]);
}
end() { end() {
this._zone.runOutsideAngular(() => this._engine.flush()); this._zone.runOutsideAngular(() => {
this._scheduleCountTask();
this._engine.flush(this._currentFlushId);
});
if (this.delegate.end) { if (this.delegate.end) {
this.delegate.end(); this.delegate.end();
} }
@ -59,11 +91,11 @@ export class AnimationRendererFactory implements RendererFactory2 {
export class AnimationRenderer implements Renderer2 { export class AnimationRenderer implements Renderer2 {
public destroyNode: ((node: any) => any)|null = null; public destroyNode: ((node: any) => any)|null = null;
private _animationCallbacksBuffer: [(e: any) => any, any][] = []; public microtaskCount: number = 0;
constructor( constructor(
public delegate: Renderer2, private _engine: AnimationEngine, private _zone: NgZone, private _factory: AnimationRendererFactory, public delegate: Renderer2,
private _namespaceId: string) { private _engine: AnimationEngine, private _zone: NgZone, private _namespaceId: string) {
this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode !(n) : null; this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode !(n) : null;
} }
@ -145,27 +177,12 @@ export class AnimationRenderer implements Renderer2 {
[name, phase] = parseTriggerCallbackName(name); [name, phase] = parseTriggerCallbackName(name);
} }
return this._engine.listen(this._namespaceId, element, name, phase, event => { return this._engine.listen(this._namespaceId, element, name, phase, event => {
this._bufferMicrotaskIntoZone(callback, event); const countId = (event as any)['_data'] || -1;
this._factory.scheduleListenerCallback(countId, callback, event);
}); });
} }
return this.delegate.listen(target, eventName, callback); return this.delegate.listen(target, eventName, callback);
} }
private _bufferMicrotaskIntoZone(fn: (e: any) => any, data: any) {
if (this._animationCallbacksBuffer.length == 0) {
Promise.resolve(null).then(() => {
this._zone.run(() => {
this._animationCallbacksBuffer.forEach(tuple => {
const [fn, data] = tuple;
fn(data);
});
this._animationCallbacksBuffer = [];
});
})
}
this._animationCallbacksBuffer.push([fn, data]);
}
} }
function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any { function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any {