fix(animations): only require one flushMicrotasks call when testing animations
This commit is contained in:
parent
eed67ddafb
commit
6cb93c1fac
|
@ -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[])
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)) :
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue