/**
 * @license
 * Copyright Google LLC 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 {animate, animateChild, AnimationEvent, AnimationOptions, AUTO_STYLE, group, keyframes, query, state, style, transition, trigger, ɵPRE_STYLE as PRE_STYLE} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver} from '@angular/animations/browser';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {ɵgetDOM as getDOM} from '@angular/common';
import {ChangeDetectorRef, Component, HostBinding, HostListener, Inject, RendererFactory2, ViewChild} from '@angular/core';
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {ɵDomRendererFactory2} from '@angular/platform-browser';
import {ANIMATION_MODULE_TYPE, BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {hasStyle} from '@angular/platform-browser/testing/src/browser_util';
import {ivyEnabled, modifiedInIvy} from '@angular/private/testing';

const DEFAULT_NAMESPACE_ID = 'id';
const DEFAULT_COMPONENT_ID = '1';

(function() {
// these tests are only mean't to be run within the DOM (for now)
if (isNode) return;

describe('animation tests', function() {
  function getLog(): MockAnimationPlayer[] {
    return MockAnimationDriver.log as MockAnimationPlayer[];
  }

  function resetLog() {
    MockAnimationDriver.log = [];
  }

  beforeEach(() => {
    resetLog();
    TestBed.configureTestingModule({
      providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
      imports: [BrowserAnimationsModule]
    });
  });

  describe('animation modules', function() {
    it('should hint at BrowserAnimationsModule being used', () => {
      TestBed.resetTestingModule();
      TestBed.configureTestingModule(
          {declarations: [SharedAnimationCmp], imports: [BrowserAnimationsModule]});

      const fixture = TestBed.createComponent(SharedAnimationCmp);
      const cmp = fixture.componentInstance;
      expect(cmp.animationType).toEqual('BrowserAnimations');
    });

    it('should hint at NoopAnimationsModule being used', () => {
      TestBed.resetTestingModule();
      TestBed.configureTestingModule(
          {declarations: [SharedAnimationCmp], imports: [NoopAnimationsModule]});

      const fixture = TestBed.createComponent(SharedAnimationCmp);
      const cmp = fixture.componentInstance;
      expect(cmp.animationType).toEqual('NoopAnimations');
    });
  });

  @Component({template: '<p>template text</p>'})
  class SharedAnimationCmp {
    constructor(@Inject(ANIMATION_MODULE_TYPE) public animationType: 'NoopAnimations'|
                'BrowserAnimations') {}
  }

  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');
       }));

    it('should always run .start callbacks before .done callbacks even for noop animations',
       fakeAsync(() => {
         @Component({
           selector: 'cmp',
           template: `
                <div [@myAnimation]="exp" (@myAnimation.start)="cb('start')" (@myAnimation.done)="cb('done')"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   transition('* => go', []),
                 ]),
           ]
         })
         class Cmp {
           exp: any = false;
           log: string[] = [];
           cb(status: string) {
             this.log.push(status);
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'go';
         fixture.detectChanges();
         expect(cmp.log).toEqual([]);

         flushMicrotasks();
         expect(cmp.log).toEqual(['start', 'done']);
       }));

    it('should emit the correct totalTime value for a noop-animation', fakeAsync(() => {
         @Component({
           selector: 'cmp',
           template: `
                <div [@myAnimation]="exp" (@myAnimation.start)="cb($event)" (@myAnimation.done)="cb($event)"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   transition(
                       '* => go',
                       [
                         animate('1s', style({opacity: 0})),
                       ]),
                 ]),
           ]
         })
         class Cmp {
           exp: any = false;
           log: AnimationEvent[] = [];
           cb(event: AnimationEvent) {
             this.log.push(event);
           }
         }

         TestBed.configureTestingModule({
           declarations: [Cmp],
           providers: [
             {provide: AnimationDriver, useClass: NoopAnimationDriver},
           ],
         });

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'go';
         fixture.detectChanges();
         expect(cmp.log).toEqual([]);

         flushMicrotasks();
         expect(cmp.log.length).toEqual(2);
         const [start, end] = cmp.log;
         expect(start.totalTime).toEqual(1000);
         expect(end.totalTime).toEqual(1000);
       }));
  });

  describe('component fixture integration', () => {
    describe('whenRenderingDone', () => {
      it('should wait until the animations are finished until continuing', fakeAsync(() => {
           @Component({
             selector: 'cmp',
             template: `
              <div [@myAnimation]="exp"></div>
            `,
             animations: [trigger(
                 'myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])])]
           })
           class Cmp {
             exp: any = false;
           }

           TestBed.configureTestingModule({declarations: [Cmp]});
           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;

           let isDone = false;
           fixture.whenRenderingDone().then(() => isDone = true);
           expect(isDone).toBe(false);

           cmp.exp = 'on';
           fixture.detectChanges();
           engine.flush();
           expect(isDone).toBe(false);

           const players = engine.players;
           expect(players.length).toEqual(1);
           players[0].finish();
           expect(isDone).toBe(false);

           flushMicrotasks();
           expect(isDone).toBe(true);
         }));

      it('should wait for a noop animation to finish before continuing', fakeAsync(() => {
           @Component({
             selector: 'cmp',
             template: `
              <div [@myAnimation]="exp"></div>
            `,
             animations: [trigger(
                 'myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])])]
           })
           class Cmp {
             exp: any = false;
           }

           TestBed.configureTestingModule({
             providers: [{provide: AnimationDriver, useClass: NoopAnimationDriver}],
             declarations: [Cmp]
           });

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;

           let isDone = false;
           fixture.whenRenderingDone().then(() => isDone = true);
           expect(isDone).toBe(false);

           cmp.exp = 'off';
           fixture.detectChanges();
           engine.flush();
           expect(isDone).toBe(false);

           flushMicrotasks();
           expect(isDone).toBe(true);
         }));

      it('should wait for active animations to finish even if they have already started',
         fakeAsync(() => {
           @Component({
             selector: 'cmp',
             template: `
                <div [@myAnimation]="exp"></div>
              `,
             animations: [trigger(
                 'myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])])]
           })
           class Cmp {
             exp: any = false;
           }

           TestBed.configureTestingModule({declarations: [Cmp]});
           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;
           cmp.exp = 'on';
           fixture.detectChanges();
           engine.flush();

           const players = engine.players;
           expect(players.length).toEqual(1);

           let isDone = false;
           fixture.whenRenderingDone().then(() => isDone = true);
           flushMicrotasks();
           expect(isDone).toBe(false);

           players[0].finish();
           flushMicrotasks();
           expect(isDone).toBe(true);
         }));
    });
  });

  describe('animation triggers', () => {
    it('should trigger a state change animation from void => state', () => {
      @Component({
        selector: 'if-cmp',
        template: `
          <div *ngIf="exp" [@myAnimation]="exp"></div>
        `,
        animations: [trigger(
            'myAnimation',
            [transition(
                'void => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
      })
      class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = true;
      fixture.detectChanges();
      engine.flush();

      expect(getLog().length).toEqual(1);
      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
      ]);
    });

    // https://github.com/angular/angular/issues/32794
    it('should support nested animation triggers', () => {
      const REUSABLE_ANIMATION = [trigger('myAnimation', [
        transition('void => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])
      ])];

      @Component({
        selector: 'if-cmp',
        template: `
          <div @myAnimation></div>
        `,
        animations: [REUSABLE_ANIMATION],
      })
      class Cmp {
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      fixture.detectChanges();
      engine.flush();

      expect(getLog().length).toEqual(1);
      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
      ]);
    });

    it('should allow a transition to use a function to determine what method to run', () => {
      let valueToMatch = '';
      let capturedElement: any;
      const transitionFn = (fromState: string, toState: string, element: any) => {
        capturedElement = element;
        return toState == valueToMatch;
      };

      @Component({
        selector: 'if-cmp',
        template: '<div #element [@myAnimation]="exp"></div>',
        animations: [
          trigger('myAnimation', [transition(
                                     transitionFn,
                                     [style({opacity: 0}), animate(1234, style({opacity: 1}))])]),
        ]
      })
      class Cmp {
        @ViewChild('element') element: any;
        exp: any = '';
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      valueToMatch = cmp.exp = 'something';
      fixture.detectChanges();
      const element = cmp.element.nativeElement;

      let players = getLog();
      expect(players.length).toEqual(1);
      let [p1] = players;
      expect(p1.totalTime).toEqual(1234);
      expect(capturedElement).toEqual(element);
      resetLog();

      valueToMatch = 'something-else';
      cmp.exp = 'this-wont-match';
      fixture.detectChanges();

      players = getLog();
      expect(players.length).toEqual(0);
    });

    it('should allow a transition to use a function to determine what method to run and expose any parameter values',
       () => {
         const transitionFn =
             (fromState: string, toState: string, element?: any, params?: {[key: string]: any}) => {
               return params!['doMatch'] == true;
             };

         @Component({
           selector: 'if-cmp',
           template: '<div [@myAnimation]="{value:exp, params: {doMatch:doMatch}}"></div>',
           animations: [
             trigger(
                 'myAnimation',
                 [transition(
                     transitionFn, [style({opacity: 0}), animate(3333, style({opacity: 1}))])]),
           ]
         })
         class Cmp {
           doMatch = false;
           exp: any = '';
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.doMatch = true;
         fixture.detectChanges();

         let players = getLog();
         expect(players.length).toEqual(1);
         let [p1] = players;
         expect(p1.totalTime).toEqual(3333);
         resetLog();

         cmp.doMatch = false;
         cmp.exp = 'this-wont-match';
         fixture.detectChanges();

         players = getLog();
         expect(players.length).toEqual(0);
       });

    it('should allow a state value to be `0`', () => {
      @Component({
        selector: 'if-cmp',
        template: `
            <div [@myAnimation]="exp"></div>
          `,
        animations: [trigger(
            'myAnimation',
            [
              transition(
                  '0 => 1', [style({height: '0px'}), animate(1234, style({height: '100px'}))]),
              transition('* => 1', [style({width: '0px'}), animate(4567, style({width: '100px'}))])
            ])]
      })
      class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = 0;
      fixture.detectChanges();
      engine.flush();
      resetLog();

      cmp.exp = 1;
      fixture.detectChanges();
      engine.flush();

      expect(getLog().length).toEqual(1);
      const player = getLog().pop()!;
      expect(player.duration).toEqual(1234);
    });

    it('should always cancel the previous transition if a follow-up transition is not matched',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div [@myAnimation]="exp" (@myAnimation.start)="callback($event)" (@myAnimation.done)="callback($event)"></div>
        `,
           animations: [trigger(
               'myAnimation',
               [transition(
                   'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
         })
         class Cmp {
           exp: any;
           startEvent: any;
           doneEvent: any;

           callback(event: any) {
             if (event.phaseName == 'done') {
               this.doneEvent = event;
             } else {
               this.startEvent = event;
             }
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'a';
         fixture.detectChanges();
         engine.flush();
         expect(getLog().length).toEqual(0);
         expect(engine.players.length).toEqual(0);

         flushMicrotasks();
         expect(cmp.startEvent.toState).toEqual('a');
         expect(cmp.startEvent.totalTime).toEqual(0);
         expect(cmp.startEvent.toState).toEqual('a');
         expect(cmp.startEvent.totalTime).toEqual(0);
         resetLog();

         cmp.exp = 'b';
         fixture.detectChanges();
         engine.flush();

         const players = getLog();
         expect(players.length).toEqual(1);
         expect(engine.players.length).toEqual(1);

         flushMicrotasks();
         expect(cmp.startEvent.toState).toEqual('b');
         expect(cmp.startEvent.totalTime).toEqual(500);
         expect(cmp.startEvent.toState).toEqual('b');
         expect(cmp.startEvent.totalTime).toEqual(500);
         resetLog();

         let completed = false;
         players[0].onDone(() => completed = true);

         cmp.exp = 'c';
         fixture.detectChanges();
         engine.flush();

         expect(engine.players.length).toEqual(0);
         expect(getLog().length).toEqual(0);

         flushMicrotasks();
         expect(cmp.startEvent.toState).toEqual('c');
         expect(cmp.startEvent.totalTime).toEqual(0);
         expect(cmp.startEvent.toState).toEqual('c');
         expect(cmp.startEvent.totalTime).toEqual(0);

         expect(completed).toBe(true);
       }));

    it('should always fire inner callbacks even if no animation is fired when a view is inserted',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div *ngIf="exp">
            <div @myAnimation (@myAnimation.start)="track($event)" (@myAnimation.done)="track($event)"></div>
          </div>
        `,
           animations: [
             trigger('myAnimation', []),
           ]
         })
         class Cmp {
           exp: any = false;
           log: string[] = [];
           track(event: any) {
             this.log.push(`${event.triggerName}-${event.phaseName}`);
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.log).toEqual([]);

         cmp.exp = true;
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.log).toEqual(['myAnimation-start', 'myAnimation-done']);
       }));

    it('should only turn a view removal as into `void` state transition', () => {
      @Component({
        selector: 'if-cmp',
        template: `
          <div *ngIf="exp1" [@myAnimation]="exp2"></div>
        `,
        animations: [trigger(
            'myAnimation',
            [
              transition(
                  'void <=> *', [style({width: '0px'}), animate(1000, style({width: '100px'}))]),
              transition(
                  '* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))]),
            ])]
      })
      class Cmp {
        exp1: any = false;
        exp2: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      function resetState() {
        cmp.exp2 = 'something';
        fixture.detectChanges();
        engine.flush();
        resetLog();
      }

      cmp.exp1 = true;
      cmp.exp2 = null;

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, width: '0px'}, {offset: 1, width: '100px'}
      ]);

      resetState();
      cmp.exp2 = false;

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, height: '0px'}, {offset: 1, height: '100px'}
      ]);

      resetState();
      cmp.exp2 = 0;

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, height: '0px'}, {offset: 1, height: '100px'}
      ]);

      resetState();
      cmp.exp2 = '';

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, height: '0px'}, {offset: 1, height: '100px'}
      ]);

      resetState();
      cmp.exp2 = undefined;

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, height: '0px'}, {offset: 1, height: '100px'}
      ]);

      resetState();
      cmp.exp1 = false;
      cmp.exp2 = 'abc';

      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, width: '0px'}, {offset: 1, width: '100px'}
      ]);
    });

    it('should stringify boolean triggers to `1` and `0`', () => {
      @Component({
        selector: 'if-cmp',
        template: `
          <div [@myAnimation]="exp"></div>
        `,
        animations: [trigger(
            'myAnimation',
            [
              transition('void => 1', [style({opacity: 0}), animate(1000, style({opacity: 1}))]),
              transition('1 => 0', [style({opacity: 1}), animate(1000, style({opacity: 0}))])
            ])]
      })
      class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.exp = true;
      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
      ]);

      cmp.exp = false;
      fixture.detectChanges();
      engine.flush();

      expect(getLog().pop()!.keyframes).toEqual([
        {offset: 0, opacity: '1'}, {offset: 1, opacity: '0'}
      ]);
    });

    it('should understand boolean values as `true` and `false` for transition animations', () => {
      @Component({
        selector: 'if-cmp',
        template: `
          <div [@myAnimation]="exp"></div>
        `,
        animations: [
          trigger(
              'myAnimation',
              [
                transition(
                    'true => false',
                    [
                      style({opacity: 0}),
                      animate(1234, style({opacity: 1})),
                    ]),
                transition(
                    'false => true',
                    [
                      style({opacity: 1}),
                      animate(4567, style({opacity: 0})),
                    ])
              ]),
        ]
      })
      class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.exp = true;
      fixture.detectChanges();

      cmp.exp = false;
      fixture.detectChanges();

      let players = getLog();
      expect(players.length).toEqual(1);
      let [player] = players;

      expect(player.duration).toEqual(1234);
    });

    it('should understand boolean values as `true` and `false` for transition animations and apply the corresponding state() value',
       () => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div [@myAnimation]="exp"></div>
        `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   state('true', style({color: 'red'})),
                   state('false', style({color: 'blue'})),
                   transition(
                       'true <=> false',
                       [
                         animate(1000, style({color: 'gold'})),
                         animate(1000),
                       ]),
                 ]),
           ]
         })
         class Cmp {
           exp: any = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = false;
         fixture.detectChanges();

         cmp.exp = true;
         fixture.detectChanges();

         let players = getLog();
         expect(players.length).toEqual(1);
         let [player] = players;

         expect(player.keyframes).toEqual([
           {color: 'blue', offset: 0},
           {color: 'gold', offset: 0.5},
           {color: 'red', offset: 1},
         ]);
       });

    it('should not throw an error if a trigger with the same name exists in separate components',
       () => {
         @Component({selector: 'cmp1', template: '...', animations: [trigger('trig', [])]})
         class Cmp1 {
         }

         @Component({selector: 'cmp2', template: '...', animations: [trigger('trig', [])]})
         class Cmp2 {
         }

         TestBed.configureTestingModule({declarations: [Cmp1, Cmp2]});
         const cmp1 = TestBed.createComponent(Cmp1);
         const cmp2 = TestBed.createComponent(Cmp2);
       });

    describe('host bindings', () => {
      it('should trigger a state change animation from state => state on the component host element',
         fakeAsync(() => {
           @Component({
             selector: 'my-cmp',
             template: '...',
             animations: [trigger(
                 'myAnimation',
                 [transition(
                     'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
           })
           class Cmp {
             @HostBinding('@myAnimation') exp = 'a';
           }

           TestBed.configureTestingModule({declarations: [Cmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;
           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toEqual(0);

           cmp.exp = 'b';
           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toEqual(1);

           const data = getLog().pop()!;
           expect(data.element).toEqual(fixture.elementRef.nativeElement);
           expect(data.keyframes).toEqual([{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}]);
         }));

      it('should trigger a leave animation when the inner components host binding updates',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             template: `
                <child-cmp *ngIf="exp"></child-cmp>
              `
           })
           class ParentCmp {
             public exp = true;
           }

           @Component({
             selector: 'child-cmp',
             template: '...',
             animations: [trigger(
                 'host',
                 [transition(':leave', [style({opacity: 1}), animate(1000, style({opacity: 0}))])])]
           })
           class ChildCmp {
             @HostBinding('@host') public hostAnimation = true;
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toEqual(0);

           cmp.exp = false;
           fixture.detectChanges();
           expect(fixture.debugElement.nativeElement.children.length).toBe(1);

           engine.flush();
           expect(getLog().length).toEqual(1);

           const [player] = getLog();
           expect(player.keyframes).toEqual([
             {opacity: '1', offset: 0},
             {opacity: '0', offset: 1},
           ]);

           player.finish();
           expect(fixture.debugElement.nativeElement.children.length).toBe(0);
         }));

      it('should wait for child animations before removing parent', fakeAsync(() => {
           @Component({
             template: '<child-cmp *ngIf="exp" @parentTrigger></child-cmp>',
             animations: [trigger(
                 'parentTrigger', [transition(':leave', [group([query('@*', animateChild())])])])]
           })
           class ParentCmp {
             exp = true;
           }

           @Component({
             selector: 'child-cmp',
             template: '<p @childTrigger>Hello there</p>',
             animations: [trigger(
                 'childTrigger',
                 [transition(
                     ':leave', [style({opacity: 1}), animate('200ms', style({opacity: 0}))])])]
           })
           class ChildCmp {
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);

           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toBe(0);

           fixture.componentInstance.exp = false;
           fixture.detectChanges();
           expect(fixture.nativeElement.children.length).toBe(1);

           engine.flush();
           expect(getLog().length).toBe(1);

           const player = getLog()[0];
           expect(player.keyframes).toEqual([
             {opacity: '1', offset: 0},
             {opacity: '0', offset: 1},
           ]);

           player.finish();
           flushMicrotasks();
           expect(fixture.nativeElement.children.length).toBe(0);
         }));

      // animationRenderer => nonAnimationRenderer
      it('should trigger a leave animation when the outer components element binding updates on the host component element',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             animations: [trigger(
                 'host',
                 [transition(
                     ':leave', [style({opacity: 1}), animate(1000, style({opacity: 0}))])])],
             template: `
                <child-cmp *ngIf="exp" @host></child-cmp>
              `
           })
           class ParentCmp {
             public exp = true;
           }

           @Component({
             selector: 'child-cmp',
             template: '...',
           })
           class ChildCmp {
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toEqual(0);

           cmp.exp = false;
           fixture.detectChanges();
           expect(fixture.debugElement.nativeElement.children.length).toBe(1);

           engine.flush();
           expect(getLog().length).toEqual(1);

           const [player] = getLog();
           expect(player.keyframes).toEqual([
             {opacity: '1', offset: 0},
             {opacity: '0', offset: 1},
           ]);

           player.finish();
           flushMicrotasks();
           expect(fixture.debugElement.nativeElement.children.length).toBe(0);
         }));

      it('should trigger a leave animation when both the inner and outer components trigger on the same element',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             animations: [trigger(
                 'host', [transition(
                             ':leave',
                             [style({height: '100px'}), animate(1000, style({height: '0px'}))])])],
             template: `
                <child-cmp *ngIf="exp" @host></child-cmp>
              `
           })
           class ParentCmp {
             public exp = true;
           }

           @Component({
             selector: 'child-cmp',
             template: '...',
             animations: [trigger(
                 'host',
                 [transition(
                     ':leave', [style({width: '100px'}), animate(1000, style({width: '0px'}))])])]
           })
           class ChildCmp {
             @HostBinding('@host') public hostAnimation = true;
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           fixture.detectChanges();
           engine.flush();
           expect(getLog().length).toEqual(0);

           cmp.exp = false;
           fixture.detectChanges();
           expect(fixture.debugElement.nativeElement.children.length).toBe(1);

           engine.flush();
           expect(getLog().length).toEqual(2);

           const [p1, p2] = getLog();
           expect(p1.keyframes).toEqual([
             {width: '100px', offset: 0},
             {width: '0px', offset: 1},
           ]);

           expect(p2.keyframes).toEqual([
             {height: '100px', offset: 0},
             {height: '0px', offset: 1},
           ]);

           p1.finish();
           p2.finish();
           flushMicrotasks();
           expect(fixture.debugElement.nativeElement.children.length).toBe(0);
         }));

      it('should not throw when the host element is removed and no animation triggers',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             template: `
                <child-cmp *ngIf="exp"></child-cmp>
              `
           })
           class ParentCmp {
             public exp = true;
           }

           @Component({
             selector: 'child-cmp',
             template: '...',
             animations: [trigger('host', [transition('a => b', [style({height: '100px'})])])],
           })
           class ChildCmp {
             @HostBinding('@host') public hostAnimation = 'a';
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           fixture.detectChanges();
           expect(fixture.debugElement.nativeElement.children.length).toBe(1);

           engine.flush();
           expect(getLog().length).toEqual(0);

           cmp.exp = false;
           fixture.detectChanges();
           engine.flush();
           flushMicrotasks();
           expect(getLog().length).toEqual(0);
           expect(fixture.debugElement.nativeElement.children.length).toBe(0);

           flushMicrotasks();
           expect(fixture.debugElement.nativeElement.children.length).toBe(0);
         }));

      it('should properly evaluate pre/auto-style values when components are inserted/removed which contain host animations',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             template: `
                <child-cmp *ngFor="let item of items"></child-cmp>
              `
           })
           class ParentCmp {
             items: any[] = [1, 2, 3, 4, 5];
           }

           @Component({
             selector: 'child-cmp',
             template: '... child ...',
             animations:
                 [trigger('host', [transition(':leave', [animate(1000, style({opacity: 0}))])])]
           })
           class ChildCmp {
             @HostBinding('@host') public hostAnimation = 'a';
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           const element = fixture.nativeElement;
           fixture.detectChanges();

           cmp.items = [0, 2, 4, 6];  // 1,3,5 get removed
           fixture.detectChanges();

           const items = element.querySelectorAll('child-cmp');
           for (let i = 0; i < items.length; i++) {
             const item = items[i];
             expect(item.style['display']).toBeFalsy();
           }
         }));
    });

    it('should cancel and merge in mid-animation styles into the follow-up animation, but only for animation keyframes that start right away',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
          <div [@myAnimation]="exp"></div>
        `,
           animations: [trigger(
               'myAnimation',
               [
                 transition(
                     'a => b',
                     [
                       style({'opacity': '0'}),
                       animate(500, style({'opacity': '1'})),
                     ]),
                 transition(
                     'b => c',
                     [
                       group([
                         animate(500, style({'width': '100px'})),
                         animate(500, style({'height': '100px'})),
                       ]),
                       animate(500, keyframes([style({'opacity': '0'}), style({'opacity': '1'})]))
                     ]),
               ])],
         })
         class Cmp {
           exp: any = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'a';
         fixture.detectChanges();
         engine.flush();
         expect(getLog().length).toEqual(0);
         resetLog();

         cmp.exp = 'b';
         fixture.detectChanges();
         engine.flush();
         expect(getLog().length).toEqual(1);
         resetLog();

         cmp.exp = 'c';
         fixture.detectChanges();
         engine.flush();

         const players = getLog();
         expect(players.length).toEqual(3);
         const [p1, p2, p3] = players;
         expect(p1.previousStyles).toEqual({opacity: AUTO_STYLE});
         expect(p2.previousStyles).toEqual({opacity: AUTO_STYLE});
         expect(p3.previousStyles).toEqual({});
       });

    it('should provide the styling of previous players that are grouped', () => {
      @Component({
          selector: 'ani-cmp',
          template: `
          <div [@myAnimation]="exp"></div>
        `,
          animations: [trigger(
              'myAnimation',
              [
                transition(
                    '1 => 2',
                    [
                      group([
                        animate(500, style({'width': '100px'})),
                        animate(500, style({'height': '100px'})),
                      ]),
                      animate(500, keyframes([
                        style({'opacity': '0'}),
                        style({'opacity': '1'})
                      ]))
                    ]),
                transition(
                    '2 => 3',
                    [
                      style({'opacity': '0'}),
                      animate(500, style({'opacity': '1'})),
                    ]),
              ])],
        })
        class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      fixture.detectChanges();
      engine.flush();

      cmp.exp = '1';
      fixture.detectChanges();
      engine.flush();
      expect(getLog().length).toEqual(0);
      resetLog();

      cmp.exp = '2';
      fixture.detectChanges();
      engine.flush();
      expect(getLog().length).toEqual(3);
      resetLog();

      cmp.exp = '3';
      fixture.detectChanges();
      engine.flush();

      const players = getLog();
      expect(players.length).toEqual(1);
      const player = players[0] as MockAnimationPlayer;
      const pp = player.previousPlayers as MockAnimationPlayer[];

      expect(pp.length).toEqual(3);
      expect(pp[0].currentSnapshot).toEqual({width: AUTO_STYLE});
      expect(pp[1].currentSnapshot).toEqual({height: AUTO_STYLE});
      expect(pp[2].currentSnapshot).toEqual({opacity: AUTO_STYLE});
    });

    it('should provide the styling of previous players that are grouped and queried and make sure match the players with the correct elements',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
          <div class="container" [@myAnimation]="exp">
            <div class="inner"></div>
          </div>
        `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   transition(
                       '1 => 2',
                       [
                         style({fontSize: '10px'}),
                         query(
                             '.inner',
                             [
                               style({fontSize: '20px'}),
                             ]),
                         animate('1s', style({fontSize: '100px'})),
                         query(
                             '.inner',
                             [
                               animate('1s', style({fontSize: '200px'})),
                             ]),
                       ]),
                   transition(
                       '2 => 3',
                       [
                         animate('1s', style({fontSize: '0px'})),
                         query(
                             '.inner',
                             [
                               animate('1s', style({fontSize: '0px'})),
                             ]),
                       ]),
                 ]),
           ],
         })
         class Cmp {
           exp: any = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         fixture.detectChanges();

         cmp.exp = '1';
         fixture.detectChanges();
         resetLog();

         cmp.exp = '2';
         fixture.detectChanges();
         resetLog();

         cmp.exp = '3';
         fixture.detectChanges();
         const players = getLog();
         expect(players.length).toEqual(2);
         const [p1, p2] = players as MockAnimationPlayer[];

         const pp1 = p1.previousPlayers as MockAnimationPlayer[];
         expect(p1.element.classList.contains('container')).toBeTruthy();
         for (let i = 0; i < pp1.length; i++) {
           expect(pp1[i].element).toEqual(p1.element);
         }

         const pp2 = p2.previousPlayers as MockAnimationPlayer[];
         expect(p2.element.classList.contains('inner')).toBeTruthy();
         for (let i = 0; i < pp2.length; i++) {
           expect(pp2[i].element).toEqual(p2.element);
         }
       });

    it('should properly balance styles between states even if there are no destination state styles',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div @myAnimation *ngIf="exp"></div>
          `,
           animations: [trigger(
               'myAnimation',
               [
                 state('void', style({opacity: 0, width: '0px', height: '0px'})),
                 transition(':enter', animate(1000))
               ])]
         })
         class Cmp {
           exp: boolean = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = true;

         fixture.detectChanges();
         engine.flush();

         const [p1] = getLog();
         expect(p1.keyframes).toEqual([
           {opacity: '0', width: '0px', height: '0px', offset: 0},
           {opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE, offset: 1}
         ]);
       });

    it('should not apply the destination styles if the final animate step already contains styles',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div @myAnimation *ngIf="exp"></div>
          `,
           animations: [trigger(
               'myAnimation',
               [
                 state('void', style({color: 'red'})), state('*', style({color: 'blue'})),
                 transition(
                     ':enter',
                     [style({fontSize: '0px '}), animate(1000, style({fontSize: '100px'}))])
               ])]
         })
         class Cmp {
           exp: boolean = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = true;

         fixture.detectChanges();
         engine.flush();

         const players = getLog();
         expect(players.length).toEqual(1);

         // notice how the final color is NOT blue
         expect(players[0].keyframes).toEqual([
           {fontSize: '0px', color: 'red', offset: 0}, {fontSize: '100px', color: 'red', offset: 1}
         ]);
       });

    it('should invoke an animation trigger that is state-less', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div *ngFor="let item of items" @myAnimation></div>
          `,
        animations: [trigger(
            'myAnimation',
            [transition(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
      })
      class Cmp {
        items: number[] = [];
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.items = [1, 2, 3, 4, 5];
      fixture.detectChanges();
      engine.flush();
      expect(getLog().length).toEqual(5);

      for (let i = 0; i < 5; i++) {
        const item = getLog()[i];
        expect(item.duration).toEqual(1000);
        expect(item.keyframes).toEqual([{opacity: '0', offset: 0}, {opacity: '1', offset: 1}]);
      }
    });

    it('should retain styles on the element once the animation is complete', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div #green @green></div>
          `,
        animations: [trigger(
            'green',
            [state('*', style({backgroundColor: 'green'})), transition('* => *', animate(500))])]
      })
      class Cmp {
        @ViewChild('green') public element: any;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      fixture.detectChanges();
      engine.flush();

      const player = engine.players.pop()!;
      player.finish();

      expect(hasStyle(cmp.element.nativeElement, 'background-color', 'green')).toBeTruthy();
    });

    it('should retain state styles when the underlying DOM structure changes even if there are no insert/remove animations',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div class="item" *ngFor="let item of items" [@color]="colorExp">
              {{ item }}
            </div>
          `,
           animations: [trigger('color', [state('green', style({backgroundColor: 'green'}))])]
         })
         class Cmp {
           public colorExp = 'green';
           public items = [0, 1, 2, 3];

           reorder() {
             const temp = this.items[0];
             this.items[0] = this.items[1];
             this.items[1] = temp;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         fixture.detectChanges();

         let elements: HTMLElement[] = fixture.nativeElement.querySelectorAll('.item');
         assertBackgroundColor(elements[0], 'green');
         assertBackgroundColor(elements[1], 'green');
         assertBackgroundColor(elements[2], 'green');
         assertBackgroundColor(elements[3], 'green');

         elements[0].title = '0a';
         elements[1].title = '1a';

         cmp.reorder();
         fixture.detectChanges();

         elements = fixture.nativeElement.querySelectorAll('.item');
         assertBackgroundColor(elements[0], 'green');
         assertBackgroundColor(elements[1], 'green');
         assertBackgroundColor(elements[2], 'green');
         assertBackgroundColor(elements[3], 'green');

         function assertBackgroundColor(element: HTMLElement, color: string) {
           expect(element.style.getPropertyValue('background-color')).toEqual(color);
         }
       });

    it('should retain state styles when the underlying DOM structure changes even if there are insert/remove animations',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div class="item" *ngFor="let item of items" [@color]="colorExp">
              {{ item }}
            </div>
          `,
           animations: [trigger(
               'color',
               [
                 transition('* => *', animate(500)),
                 state('green', style({backgroundColor: 'green'}))
               ])]
         })
         class Cmp {
           public colorExp = 'green';
           public items = [0, 1, 2, 3];

           reorder() {
             const temp = this.items[0];
             this.items[0] = this.items[1];
             this.items[1] = temp;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         fixture.detectChanges();

         getLog().forEach(p => p.finish());

         let elements: HTMLElement[] = fixture.nativeElement.querySelectorAll('.item');
         assertBackgroundColor(elements[0], 'green');
         assertBackgroundColor(elements[1], 'green');
         assertBackgroundColor(elements[2], 'green');
         assertBackgroundColor(elements[3], 'green');

         elements[0].title = '0a';
         elements[1].title = '1a';

         cmp.reorder();
         fixture.detectChanges();

         getLog().forEach(p => p.finish());

         elements = fixture.nativeElement.querySelectorAll('.item');
         assertBackgroundColor(elements[0], 'green');
         assertBackgroundColor(elements[1], 'green');
         assertBackgroundColor(elements[2], 'green');
         assertBackgroundColor(elements[3], 'green');

         function assertBackgroundColor(element: HTMLElement, color: string) {
           expect(element.style.getPropertyValue('background-color')).toEqual(color);
         }
       });

    it('should animate removals of nodes to the `void` state for each animation trigger, but treat all auto styles as pre styles',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div *ngIf="exp" class="ng-if" [@trig1]="exp2" @trig2></div>
          `,
           animations: [
             trigger('trig1', [transition('state => void', [animate(1000, style({opacity: 0}))])]),
             trigger('trig2', [transition(':leave', [animate(1000, style({width: '0px'}))])])
           ]
         })
         class Cmp {
           public exp = true;
           public exp2 = 'state';
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = true;
         fixture.detectChanges();
         engine.flush();
         resetLog();

         const element = fixture.nativeElement.querySelector('.ng-if');
         assertHasParent(element, true);

         cmp.exp = false;
         fixture.detectChanges();
         engine.flush();

         assertHasParent(element, true);

         expect(getLog().length).toEqual(2);

         const player2 = getLog().pop()!;
         const player1 = getLog().pop()!;

         expect(player2.keyframes).toEqual([
           {width: PRE_STYLE, offset: 0},
           {width: '0px', offset: 1},
         ]);

         expect(player1.keyframes).toEqual([
           {opacity: PRE_STYLE, offset: 0}, {opacity: '0', offset: 1}
         ]);

         player2.finish();
         player1.finish();
         assertHasParent(element, false);
       });

    it('should properly cancel all existing animations when a removal occurs', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div *ngIf="exp" [@myAnimation]="exp"></div>
          `,
        animations: [
          trigger(
              'myAnimation',
              [
                transition(
                    '* => go', [style({width: '0px'}), animate(1000, style({width: '100px'}))]),
              ]),
        ]
      })
      class Cmp {
        // TODO(issue/24571): remove '!'.
        public exp!: string|null;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = 'go';
      fixture.detectChanges();
      engine.flush();
      expect(getLog().length).toEqual(1);
      const [player1] = getLog();
      resetLog();

      let finished = false;
      player1.onDone(() => finished = true);

      let destroyed = false;
      player1.onDestroy(() => destroyed = true);

      cmp.exp = null;
      fixture.detectChanges();
      engine.flush();

      expect(finished).toBeTruthy();
      expect(destroyed).toBeTruthy();
    });

    it('should not run inner child animations when a parent is set to be removed', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div *ngIf="exp" class="parent" >
              <div [@myAnimation]="exp2"></div>
            </div>
          `,
        animations:
            [trigger('myAnimation', [transition('a => b', [animate(1000, style({width: '0px'}))])])]
      })
      class Cmp {
        public exp = true;
        public exp2 = '0';
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.exp = true;
      cmp.exp2 = 'a';
      fixture.detectChanges();
      engine.flush();
      resetLog();

      cmp.exp = false;
      cmp.exp2 = 'b';
      fixture.detectChanges();
      engine.flush();
      expect(getLog().length).toEqual(0);
    });

    it('should cancel all active inner child animations when a parent removal animation is set to go',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div *ngIf="exp1" @parent>
              <div [@child]="exp2" class="child1"></div>
              <div [@child]="exp2" class="child2"></div>
            </div>
          `,
           animations: [
             trigger(
                 'parent',
                 [transition(':leave', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
             trigger(
                 'child',
                 [transition('a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])
           ]
         })
         class Cmp {
           public exp1: any;
           public exp2: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp1 = true;
         cmp.exp2 = 'a';
         fixture.detectChanges();
         engine.flush();
         resetLog();

         cmp.exp2 = 'b';
         fixture.detectChanges();
         engine.flush();

         let players = getLog();
         expect(players.length).toEqual(2);
         const [p1, p2] = players;

         let count = 0;
         p1.onDone(() => count++);
         p2.onDone(() => count++);

         cmp.exp1 = false;
         fixture.detectChanges();
         engine.flush();

         expect(count).toEqual(2);
       });

    it('should destroy inner animations when a parent node is set for removal', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div #parent class="parent">
              <div [@child]="exp" class="child1"></div>
              <div [@child]="exp" class="child2"></div>
            </div>
          `,
        animations: [trigger(
            'child',
            [transition('a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
      })
      class Cmp {
        public exp: any;

        @ViewChild('parent') public parentElement: any;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      const someTrigger = trigger('someTrigger', []);
      const hostElement = fixture.nativeElement;
      engine.register(DEFAULT_NAMESPACE_ID, hostElement);
      engine.registerTrigger(
          DEFAULT_COMPONENT_ID, DEFAULT_NAMESPACE_ID, hostElement, someTrigger.name, someTrigger);

      cmp.exp = 'a';
      fixture.detectChanges();
      engine.flush();
      resetLog();

      cmp.exp = 'b';
      fixture.detectChanges();
      engine.flush();

      const players = getLog();
      expect(players.length).toEqual(2);
      const [p1, p2] = players;

      let count = 0;
      p1.onDone(() => count++);
      p2.onDone(() => count++);

      engine.onRemove(DEFAULT_NAMESPACE_ID, cmp.parentElement.nativeElement, null);
      expect(count).toEqual(2);
    });

    it('should allow inner removals to happen when a non removal-based parent animation is set to animate',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div #parent [@parent]="exp1" class="parent">
              <div #child *ngIf="exp2" class="child"></div>
            </div>
          `,
           animations: [trigger(
               'parent',
               [transition('a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
         })
         class Cmp {
           public exp1: any;
           public exp2: any;

           @ViewChild('parent') public parent: any;

           @ViewChild('child') public child: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp1 = 'a';
         cmp.exp2 = true;
         fixture.detectChanges();
         engine.flush();
         resetLog();

         cmp.exp1 = 'b';
         fixture.detectChanges();
         engine.flush();

         const player = getLog()[0];
         const p = cmp.parent.nativeElement;
         const c = cmp.child.nativeElement;

         expect(p.contains(c)).toBeTruthy();

         cmp.exp2 = false;
         fixture.detectChanges();
         engine.flush();

         expect(p.contains(c)).toBeFalsy();

         player.finish();

         expect(p.contains(c)).toBeFalsy();
       });

    it('should make inner removals wait until a parent based removal animation has finished',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div #parent *ngIf="exp1" @parent class="parent">
              <div #child1 *ngIf="exp2" class="child1"></div>
              <div #child2 *ngIf="exp2" class="child2"></div>
            </div>
          `,
           animations: [trigger(
               'parent',
               [transition(':leave', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
         })
         class Cmp {
           public exp1: any;
           public exp2: any;

           @ViewChild('parent') public parent: any;

           @ViewChild('child1') public child1Elm: any;

           @ViewChild('child2') public child2Elm: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp1 = true;
         cmp.exp2 = true;
         fixture.detectChanges();
         engine.flush();
         resetLog();

         const p = cmp.parent.nativeElement;
         const c1 = cmp.child1Elm.nativeElement;
         const c2 = cmp.child2Elm.nativeElement;

         cmp.exp1 = false;
         cmp.exp2 = false;
         fixture.detectChanges();
         engine.flush();

         expect(p.contains(c1)).toBeTruthy();
         expect(p.contains(c2)).toBeTruthy();

         cmp.exp2 = false;
         fixture.detectChanges();
         engine.flush();

         expect(p.contains(c1)).toBeTruthy();
         expect(p.contains(c2)).toBeTruthy();
       });

    it('should detect trigger changes based on object.value properties', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div [@myAnimation]="{value:exp}"></div>
          `,
        animations: [
          trigger(
              'myAnimation',
              [
                transition('* => 1', [animate(1234, style({opacity: 0}))]),
                transition('* => 2', [animate(5678, style({opacity: 0}))]),
              ]),
        ]
      })
      class Cmp {
        public exp: any;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.exp = '1';
      fixture.detectChanges();
      engine.flush();
      let players = getLog();
      expect(players.length).toEqual(1);
      expect(players[0].duration).toEqual(1234);
      resetLog();

      cmp.exp = '2';
      fixture.detectChanges();
      engine.flush();
      players = getLog();
      expect(players.length).toEqual(1);
      expect(players[0].duration).toEqual(5678);
    });

    it('should not render animations when the object expression value is the same as it was previously',
       () => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div [@myAnimation]="{value:exp,params:params}"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   transition('* => *', [animate(1234, style({opacity: 0}))]),
                 ]),
           ]
         })
         class Cmp {
           public exp: any;
           public params: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = '1';
         cmp.params = {};
         fixture.detectChanges();
         engine.flush();
         let players = getLog();
         expect(players.length).toEqual(1);
         expect(players[0].duration).toEqual(1234);
         resetLog();

         cmp.exp = '1';
         cmp.params = {};
         fixture.detectChanges();
         engine.flush();
         players = getLog();
         expect(players.length).toEqual(0);
       });

    it('should update the final state styles when params update even if the expression hasn\'t changed',
       fakeAsync(() => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div [@myAnimation]="{value:exp,params:{color:color}}"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   state('*', style({color: '{{ color }}'}), {params: {color: 'black'}}),
                   transition('* => 1', animate(500))
                 ]),
           ]
         })
         class Cmp {
           public exp: any;
           // TODO(issue/24571): remove '!'.
           public color!: string|null;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = '1';
         cmp.color = 'red';
         fixture.detectChanges();
         const player = getLog()[0]!;
         const element = player.element;
         player.finish();

         flushMicrotasks();
         expect(hasStyle(element, 'color', 'red')).toBeTruthy();

         cmp.exp = '1';
         cmp.color = 'blue';
         fixture.detectChanges();
         resetLog();

         flushMicrotasks();
         expect(hasStyle(element, 'color', 'blue')).toBeTruthy();

         cmp.exp = '1';
         cmp.color = null;
         fixture.detectChanges();
         resetLog();

         flushMicrotasks();
         expect(hasStyle(element, 'color', 'black')).toBeTruthy();
       }));

    it('should substitute in values if the provided state match is an object with values', () => {
      @Component({
        selector: 'ani-cmp',
        template: `
            <div [@myAnimation]="exp"></div>
          `,
        animations: [trigger(
            'myAnimation',
            [transition(
                'a => b',
                [style({opacity: '{{ start }}'}), animate(1000, style({opacity: '{{ end }}'}))],
                buildParams({start: '0', end: '1'}))])]
      })
      class Cmp {
        public exp: any;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;

      cmp.exp = {value: 'a'};
      fixture.detectChanges();
      engine.flush();
      resetLog();

      cmp.exp = {value: 'b', params: {start: .3, end: .6}};
      fixture.detectChanges();
      engine.flush();
      const player = getLog().pop()!;
      expect(player.keyframes).toEqual([{opacity: '0.3', offset: 0}, {opacity: '0.6', offset: 1}]);
    });

    it('should retain substituted styles on the element once the animation is complete if referenced in the final state',
       fakeAsync(() => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div [@myAnimation]="{value:exp, params: { color: color }}"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   state(
                       'start', style({
                         color: '{{ color }}',
                         fontSize: '{{ fontSize }}px',
                         width: '{{ width }}'
                       }),
                       {params: {color: 'red', fontSize: '200', width: '10px'}}),

                   state(
                       'final',
                       style({color: '{{ color }}', fontSize: '{{ fontSize }}px', width: '888px'}),
                       {params: {color: 'green', fontSize: '50', width: '100px'}}),

                   transition('start => final', animate(500)),
                 ]),
           ]
         })
         class Cmp {
           public exp: any;
           public color: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'start';
         cmp.color = 'red';
         fixture.detectChanges();
         resetLog();

         cmp.exp = 'final';
         cmp.color = 'blue';
         fixture.detectChanges();

         const players = getLog();
         expect(players.length).toEqual(1);
         const [p1] = players;

         expect(p1.keyframes).toEqual([
           {color: 'red', fontSize: '200px', width: '10px', offset: 0},
           {color: 'blue', fontSize: '50px', width: '888px', offset: 1}
         ]);

         const element = p1.element;
         p1.finish();
         flushMicrotasks();

         expect(hasStyle(element, 'color', 'blue')).toBeTruthy();
         expect(hasStyle(element, 'fontSize', '50px')).toBeTruthy();
         expect(hasStyle(element, 'width', '888px')).toBeTruthy();
       }));

    it('should only evaluate final state param substitutions from the expression and state values and not from the transition options ',
       fakeAsync(() => {
         @Component({
           selector: 'ani-cmp',
           template: `
            <div [@myAnimation]="exp"></div>
          `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   state(
                       'start', style({
                         width: '{{ width }}',
                         height: '{{ height }}',
                       }),
                       {params: {width: '0px', height: '0px'}}),

                   state(
                       'final', style({
                         width: '{{ width }}',
                         height: '{{ height }}',
                       }),
                       {params: {width: '100px', height: '100px'}}),

                   transition(
                       'start => final', [animate(500)],
                       {params: {width: '333px', height: '666px'}}),
                 ]),
           ]
         })
         class Cmp {
           public exp: any;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'start';
         fixture.detectChanges();
         resetLog();

         cmp.exp = 'final';
         fixture.detectChanges();

         const players = getLog();
         expect(players.length).toEqual(1);
         const [p1] = players;

         expect(p1.keyframes).toEqual([
           {width: '0px', height: '0px', offset: 0},
           {width: '100px', height: '100px', offset: 1},
         ]);

         const element = p1.element;
         p1.finish();
         flushMicrotasks();

         expect(hasStyle(element, 'width', '100px')).toBeTruthy();
         expect(hasStyle(element, 'height', '100px')).toBeTruthy();
       }));

    it('should not flush animations twice when an inner component runs change detection', () => {
      @Component({
        selector: 'outer-cmp',
        template: `
            <div *ngIf="exp" @outer></div>
            <inner-cmp #inner></inner-cmp>
          `,
        animations: [trigger(
            'outer',
            [transition(':enter', [style({opacity: 0}), animate('1s', style({opacity: 1}))])])]
      })
      class OuterCmp {
        @ViewChild('inner') public inner: any;
        public exp: any = null;

        update() {
          this.exp = 'go';
        }

        ngDoCheck() {
          if (this.exp == 'go') {
            this.inner.update();
          }
        }
      }

      @Component({
        selector: 'inner-cmp',
        template: `
            <div *ngIf="exp" @inner></div>
          `,
        animations: [trigger('inner', [transition(
                                          ':enter',
                                          [
                                            style({opacity: 0}),
                                            animate('1s', style({opacity: 1})),
                                          ])])]
      })
      class InnerCmp {
        public exp: any;
        constructor(private _ref: ChangeDetectorRef) {}
        update() {
          this.exp = 'go';
          this._ref.detectChanges();
        }
      }

      TestBed.configureTestingModule({declarations: [OuterCmp, InnerCmp]});

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(OuterCmp);
      const cmp = fixture.componentInstance;
      fixture.detectChanges();
      expect(getLog()).toEqual([]);

      cmp.update();
      fixture.detectChanges();

      const players = getLog();
      expect(players.length).toEqual(2);
    });

    describe('transition aliases', () => {
      describe(':increment', () => {
        it('should detect when a value has incremented', () => {
          @Component({
            selector: 'if-cmp',
            template: `
          <div [@myAnimation]="exp"></div>
        `,
            animations: [
              trigger(
                  'myAnimation',
                  [
                    transition(
                        ':increment',
                        [
                          animate(1234, style({background: 'red'})),
                        ]),
                  ]),
            ]
          })
          class Cmp {
            exp: number = 0;
          }

          TestBed.configureTestingModule({declarations: [Cmp]});
          const fixture = TestBed.createComponent(Cmp);
          const cmp = fixture.componentInstance;
          fixture.detectChanges();
          let players = getLog();
          expect(players.length).toEqual(0);

          cmp.exp++;
          fixture.detectChanges();
          players = getLog();
          expect(players.length).toEqual(1);
          expect(players[0].duration).toEqual(1234);
          resetLog();

          cmp.exp = 5;
          fixture.detectChanges();
          players = getLog();
          expect(players.length).toEqual(1);
          expect(players[0].duration).toEqual(1234);
        });
      });

      describe(':decrement', () => {
        it('should detect when a value has decremented', () => {
          @Component({
            selector: 'if-cmp',
            template: `
          <div [@myAnimation]="exp"></div>
        `,
            animations: [
              trigger(
                  'myAnimation',
                  [
                    transition(
                        ':decrement',
                        [
                          animate(1234, style({background: 'red'})),
                        ]),
                  ]),
            ]
          })
          class Cmp {
            exp: number = 5;
          }

          TestBed.configureTestingModule({declarations: [Cmp]});
          const fixture = TestBed.createComponent(Cmp);
          const cmp = fixture.componentInstance;
          fixture.detectChanges();
          let players = getLog();
          expect(players.length).toEqual(0);

          cmp.exp--;
          fixture.detectChanges();
          players = getLog();
          expect(players.length).toEqual(1);
          expect(players[0].duration).toEqual(1234);
          resetLog();

          cmp.exp = 0;
          fixture.detectChanges();
          players = getLog();
          expect(players.length).toEqual(1);
          expect(players[0].duration).toEqual(1234);
        });
      });
    });

    it('should animate nodes properly when they have been re-ordered', () => {
      @Component({
        selector: 'if-cmp',
        template: `
                <div *ngFor="let item of items" [class]="'class-' + item.value">
                  <div [@myAnimation]="item.count">
                    {{ item.value }}
                  </div>
                </div>
              `,
        animations: [
          trigger(
              'myAnimation',
              [
                state('0', style({opacity: 0})), state('1', style({opacity: 0.4})),
                state('2', style({opacity: 0.8})), transition('* => 1, * => 2', [animate(1000)])
              ]),
        ]
      })
      class Cmp {
        items = [
          {value: '1', count: 0},
          {value: '2', count: 0},
          {value: '3', count: 0},
          {value: '4', count: 0},
          {value: '5', count: 0},
        ];

        reOrder() {
          this.items = [
            this.items[4],
            this.items[1],
            this.items[3],
            this.items[0],
            this.items[2],
          ];
        }
      }

      TestBed.configureTestingModule({declarations: [Cmp]});
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      const one = cmp.items[0];
      const two = cmp.items[1];
      one.count++;
      fixture.detectChanges();

      cmp.reOrder();
      fixture.detectChanges();
      resetLog();

      one.count++;
      two.count++;
      fixture.detectChanges();

      const players = getLog();
      expect(players.length).toEqual(2);
    });
  });

  describe('animation listeners', () => {
    it('should trigger a `start` state change listener for when the animation changes state from void => state',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div *ngIf="exp" [@myAnimation]="exp" (@myAnimation.start)="callback($event)"></div>
        `,
           animations: [trigger(
               'myAnimation',
               [transition(
                   'void => *',
                   [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
         })
         class Cmp {
           exp: any = false;
           // TODO(issue/24571): remove '!'.
           event!: AnimationEvent;

           callback = (event: any) => {
             this.event = event;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'true';
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.event.triggerName).toEqual('myAnimation');
         expect(cmp.event.phaseName).toEqual('start');
         expect(cmp.event.totalTime).toEqual(500);
         expect(cmp.event.fromState).toEqual('void');
         expect(cmp.event.toState).toEqual('true');
       }));

    it('should trigger a `done` state change listener for when the animation changes state from a => b',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div *ngIf="exp" [@myAnimation123]="exp" (@myAnimation123.done)="callback($event)"></div>
        `,
           animations: [trigger(
               'myAnimation123',
               [transition(
                   '* => b', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))])])],
         })
         class Cmp {
           exp: any = false;
           // TODO(issue/24571): remove '!'.
           event!: AnimationEvent;

           callback = (event: any) => {
             this.event = event;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'b';
         fixture.detectChanges();
         engine.flush();

         expect(cmp.event).toBeFalsy();

         const player = engine.players.pop()!;
         player.finish();
         flushMicrotasks();

         expect(cmp.event.triggerName).toEqual('myAnimation123');
         expect(cmp.event.phaseName).toEqual('done');
         expect(cmp.event.totalTime).toEqual(999);
         expect(cmp.event.fromState).toEqual('void');
         expect(cmp.event.toState).toEqual('b');
       }));

    it('should handle callbacks for multiple triggers running simultaneously', fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div [@ani1]="exp1" (@ani1.done)="callback1($event)"></div>
          <div [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
        `,
           animations: [
             trigger(
                 'ani1',
                 [
                   transition(
                       '* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
                 ]),
             trigger(
                 'ani2',
                 [
                   transition(
                       '* => b',
                       [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
                 ])
           ],
         })
         class Cmp {
           exp1: any = false;
           exp2: any = false;
           // TODO(issue/24571): remove '!'.
           event1!: AnimationEvent;
           // TODO(issue/24571): remove '!'.
           event2!: AnimationEvent;
           // tslint:disable:semicolon
           callback1 = (event: any) => {
             this.event1 = event;
           };
           // tslint:disable:semicolon
           callback2 = (event: any) => {
             this.event2 = event;
           };
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp1 = 'a';
         cmp.exp2 = 'b';
         fixture.detectChanges();
         engine.flush();

         expect(cmp.event1).toBeFalsy();
         expect(cmp.event2).toBeFalsy();

         const player1 = engine.players[0];
         const player2 = engine.players[1];

         player1.finish();
         player2.finish();
         expect(cmp.event1).toBeFalsy();
         expect(cmp.event2).toBeFalsy();

         flushMicrotasks();
         expect(cmp.event1.triggerName).toBeTruthy('ani1');
         expect(cmp.event2.triggerName).toBeTruthy('ani2');
       }));

    it('should handle callbacks for multiple triggers running simultaneously on the same element',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
          <div [@ani1]="exp1" (@ani1.done)="callback1($event)" [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
        `,
           animations: [
             trigger(
                 'ani1',
                 [
                   transition(
                       '* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
                 ]),
             trigger(
                 'ani2',
                 [
                   transition(
                       '* => b',
                       [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
                 ])
           ],
         })
         class Cmp {
           exp1: any = false;
           exp2: any = false;
           // TODO(issue/24571): remove '!'.
           event1!: AnimationEvent;
           // TODO(issue/24571): remove '!'.
           event2!: AnimationEvent;
           callback1 = (event: any) => {
             this.event1 = event;
           };
           callback2 = (event: any) => {
             this.event2 = event;
           };
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp1 = 'a';
         cmp.exp2 = 'b';
         fixture.detectChanges();
         engine.flush();

         expect(cmp.event1).toBeFalsy();
         expect(cmp.event2).toBeFalsy();

         const player1 = engine.players[0];
         const player2 = engine.players[1];

         player1.finish();
         player2.finish();
         expect(cmp.event1).toBeFalsy();
         expect(cmp.event2).toBeFalsy();

         flushMicrotasks();
         expect(cmp.event1.triggerName).toBeTruthy('ani1');
         expect(cmp.event2.triggerName).toBeTruthy('ani2');
       }));

    it('should handle a leave animation for multiple triggers even if not all triggers have their own leave transition specified',
       fakeAsync(() => {
         @Component({
           selector: 'if-cmp',
           template: `
               <div *ngIf="exp" @foo @bar>123</div>
             `,
           animations: [
             trigger(
                 'foo',
                 [
                   transition(
                       ':enter',
                       [
                         style({opacity: 0}),
                         animate(1000, style({opacity: 1})),
                       ]),
                 ]),
             trigger(
                 'bar',
                 [
                   transition(
                       ':leave',
                       [
                         animate(1000, style({opacity: 0})),
                       ]),
                 ])
           ],
         })
         class Cmp {
           exp: boolean = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});
         const fixture = TestBed.createComponent(Cmp);
         const elm = fixture.elementRef.nativeElement;
         const cmp = fixture.componentInstance;

         cmp.exp = true;
         fixture.detectChanges();

         let players = getLog();
         expect(players.length).toEqual(1);
         let [p1] = players;
         p1.finish();
         flushMicrotasks();
         expect(elm.innerText.trim()).toEqual('123');

         resetLog();
         cmp.exp = false;
         fixture.detectChanges();

         players = getLog();
         expect(players.length).toEqual(1);
         [p1] = players;
         p1.finish();
         flushMicrotasks();
         expect(elm.innerText.trim()).toEqual('');
       }));

    it('should trigger a state change listener for when the animation changes state from void => state on the host element',
       fakeAsync(() => {
         @Component({
           selector: 'my-cmp',
           template: `...`,
           animations: [trigger(
               'myAnimation2',
               [transition(
                   'void => *',
                   [style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])])],
         })
         class Cmp {
           // TODO(issue/24571): remove '!'.
           event!: AnimationEvent;

           @HostBinding('@myAnimation2') exp: any = false;

           @HostListener('@myAnimation2.start', ['$event'])
           callback = (event: any) => {
             this.event = event;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'TRUE';
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.event.triggerName).toEqual('myAnimation2');
         expect(cmp.event.phaseName).toEqual('start');
         expect(cmp.event.totalTime).toEqual(1000);
         expect(cmp.event.fromState).toEqual('void');
         expect(cmp.event.toState).toEqual('TRUE');
       }));

    it('should always fire callbacks even when a transition is not detected', fakeAsync(() => {
         @Component({
           selector: 'my-cmp',
           template: `
              <div [@myAnimation]="exp" (@myAnimation.start)="callback($event)" (@myAnimation.done)="callback($event)"></div>
            `,
           animations: [trigger('myAnimation', [])]
         })
         class Cmp {
           // TODO(issue/24571): remove '!'.
           exp!: string;
           log: any[] = [];
           callback = (event: any) => this.log.push(`${event.phaseName} => ${event.toState}`);
         }

         TestBed.configureTestingModule({
           providers: [{provide: AnimationDriver, useClass: NoopAnimationDriver}],
           declarations: [Cmp]
         });

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = 'a';
         fixture.detectChanges();
         flushMicrotasks();
         expect(cmp.log).toEqual(['start => a', 'done => a']);

         cmp.log = [];
         cmp.exp = 'b';
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.log).toEqual(['start => b', 'done => b']);
       }));

    it('should fire callback events for leave animations even if there is no leave transition',
       fakeAsync(() => {
         @Component({
           selector: 'my-cmp',
           template: `
              <div *ngIf="exp" @myAnimation (@myAnimation.start)="callback($event)" (@myAnimation.done)="callback($event)"></div>
            `,
           animations: [trigger('myAnimation', [])]
         })
         class Cmp {
           exp: boolean = false;
           log: any[] = [];
           callback = (event: any) => {
             const state = event.toState || '_default_';
             this.log.push(`${event.phaseName} => ${state}`);
           }
         }

         TestBed.configureTestingModule({
           providers: [{provide: AnimationDriver, useClass: NoopAnimationDriver}],
           declarations: [Cmp]
         });

         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;

         cmp.exp = true;
         fixture.detectChanges();
         flushMicrotasks();
         expect(cmp.log).toEqual(['start => _default_', 'done => _default_']);

         cmp.log = [];

         cmp.exp = false;
         fixture.detectChanges();
         flushMicrotasks();

         expect(cmp.log).toEqual(['start => void', 'done => void']);
       }));

    it('should fire callbacks on a sub animation once it starts and finishes', fakeAsync(() => {
         @Component({
           selector: 'my-cmp',
           template: `
              <div class="parent"
                  [@parent]="exp1"
                  (@parent.start)="cb('parent-start',$event)"
                  (@parent.done)="cb('parent-done', $event)">
                <div class="child"
                  [@child]="exp2"
                  (@child.start)="cb('child-start',$event)"
                  (@child.done)="cb('child-done', $event)"></div>
              </div>
            `,
           animations: [
             trigger(
                 'parent',
                 [
                   transition(
                       '* => go',
                       [
                         style({width: '0px'}),
                         animate(1000, style({width: '100px'})),
                         query(
                             '.child',
                             [
                               animateChild({duration: '1s'}),
                             ]),
                         animate(1000, style({width: '0px'})),
                       ]),
                 ]),
             trigger(
                 'child',
                 [
                   transition(
                       '* => go',
                       [
                         style({height: '0px'}),
                         animate(1000, style({height: '100px'})),
                       ]),
                 ])
           ]
         })
         class Cmp {
           log: string[] = [];
           // TODO(issue/24571): remove '!'.
           exp1!: string;
           // TODO(issue/24571): remove '!'.
           exp2!: string;

           cb(name: string, event: AnimationEvent) {
             this.log.push(name);
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp1 = 'go';
         cmp.exp2 = 'go';
         fixture.detectChanges();
         engine.flush();
         flushMicrotasks();

         expect(cmp.log).toEqual(['parent-start', 'child-start']);
         cmp.log = [];

         const players = getLog();
         expect(players.length).toEqual(3);
         const [p1, p2, p3] = players;

         p1.finish();
         flushMicrotasks();
         expect(cmp.log).toEqual([]);

         p2.finish();
         flushMicrotasks();
         expect(cmp.log).toEqual([]);

         p3.finish();
         flushMicrotasks();
         expect(cmp.log).toEqual(['parent-done', 'child-done']);
       }));

    it('should fire callbacks and collect the correct the totalTime and element details for any queried sub animations',
       fakeAsync(() => {
         @Component({
           selector: 'my-cmp',
           template: `
              <div class="parent" [@parent]="exp" (@parent.done)="cb('all','done', $event)">
                <div *ngFor="let item of items"
                     class="item item-{{ item }}"
                     @child
                     (@child.start)="cb('c-' + item, 'start', $event)"
                     (@child.done)="cb('c-' + item, 'done', $event)">
                  {{ item }}
                </div>
              </div>
            `,
           animations: [
             trigger(
                 'parent',
                 [transition(
                     '* => go',
                     [
                       style({opacity: 0}), animate('1s', style({opacity: 1})),
                       query('.item', [style({opacity: 0}), animate(1000, style({opacity: 1}))]),
                       query('.item', [animateChild({duration: '1.8s', delay: '300ms'})])
                     ])]),
             trigger(
                 'child',
                 [transition(':enter', [style({opacity: 0}), animate(1500, style({opacity: 1}))])])
           ]
         })
         class Cmp {
           log: string[] = [];
           events: {[name: string]: any} = {};
           // TODO(issue/24571): remove '!'.
           exp!: string;
           items: any = [0, 1, 2, 3];

           cb(name: string, phase: string, event: AnimationEvent) {
             this.log.push(name + '-' + phase);
             this.events[name] = event;
           }
         }

         TestBed.configureTestingModule({declarations: [Cmp]});

         const engine = TestBed.inject(ɵAnimationEngine);
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'go';
         fixture.detectChanges();
         engine.flush();
         flushMicrotasks();

         expect(cmp.log).toEqual(['c-0-start', 'c-1-start', 'c-2-start', 'c-3-start']);
         cmp.log = [];

         const players = getLog();
         // 1 + 4 + 4 = 9 players
         expect(players.length).toEqual(9);

         const [pA, pq1a, pq1b, pq1c, pq1d, pq2a, pq2b, pq2c, pq2d] = getLog();
         pA.finish();
         pq1a.finish();
         pq1b.finish();
         pq1c.finish();
         pq1d.finish();
         flushMicrotasks();

         expect(cmp.log).toEqual([]);
         pq2a.finish();
         pq2b.finish();
         pq2c.finish();
         pq2d.finish();
         flushMicrotasks();

         expect(cmp.log).toEqual(['all-done', 'c-0-done', 'c-1-done', 'c-2-done', 'c-3-done']);

         expect(cmp.events['c-0'].totalTime).toEqual(4100);  // 1000 + 1000 + 1800 + 300
         expect(cmp.events['c-0'].element.innerText.trim()).toEqual('0');
         expect(cmp.events['c-1'].totalTime).toEqual(4100);
         expect(cmp.events['c-1'].element.innerText.trim()).toEqual('1');
         expect(cmp.events['c-2'].totalTime).toEqual(4100);
         expect(cmp.events['c-2'].element.innerText.trim()).toEqual('2');
         expect(cmp.events['c-3'].totalTime).toEqual(4100);
         expect(cmp.events['c-3'].element.innerText.trim()).toEqual('3');
       }));
  });

  describe('animation control flags', () => {
    describe('[@.disabled]', () => {
      it('should disable child animations when set to true', () => {
        @Component({
          selector: 'if-cmp',
          template: `
              <div [@.disabled]="disableExp">
                <div [@myAnimation]="exp"></div>
              </div>
            `,
          animations: [
            trigger(
                'myAnimation',
                [
                  transition(
                      '* => 1, * => 2',
                      [
                        animate(1234, style({width: '100px'})),
                      ]),
                ]),
          ]
        })
        class Cmp {
          exp: any = false;
          disableExp = false;
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        fixture.detectChanges();
        resetLog();

        cmp.disableExp = true;
        cmp.exp = '1';
        fixture.detectChanges();

        let players = getLog();
        expect(players.length).toEqual(0);

        cmp.disableExp = false;
        cmp.exp = '2';
        fixture.detectChanges();

        players = getLog();
        expect(players.length).toEqual(1);
        expect(players[0].totalTime).toEqual(1234);
      });

      it('should ensure state() values are applied when an animation is disabled', () => {
        @Component({
          selector: 'if-cmp',
          template: `
              <div [@.disabled]="disableExp">
                <div [@myAnimation]="exp" #elm></div>
              </div>
            `,
          animations: [
            trigger(
                'myAnimation',
                [
                  state('1', style({height: '100px'})), state('2', style({height: '200px'})),
                  state('3', style({height: '300px'})), transition('* => *', animate(500))
                ]),
          ]
        })
        class Cmp {
          exp: any = false;
          disableExp = false;

          @ViewChild('elm', {static: true}) public element: any;
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const engine = TestBed.inject(ɵAnimationEngine);

        function assertHeight(element: any, height: string) {
          expect(element.style['height']).toEqual(height);
        }

        // In Ivy, change detection needs to run before the ViewQuery for cmp.element will
        // resolve. Keeping this test enabled since we still want to test the animation logic.
        if (ivyEnabled) fixture.detectChanges();

        const cmp = fixture.componentInstance;
        const element = cmp.element.nativeElement;
        fixture.detectChanges();

        cmp.disableExp = true;
        cmp.exp = '1';
        fixture.detectChanges();
        assertHeight(element, '100px');

        cmp.exp = '2';
        fixture.detectChanges();
        assertHeight(element, '200px');

        cmp.exp = '3';
        fixture.detectChanges();
        assertHeight(element, '300px');
      });

      it('should disable animations for the element that they are disabled on', () => {
        @Component({
          selector: 'if-cmp',
          template: `
              <div [@.disabled]="disableExp" [@myAnimation]="exp"></div>
            `,
          animations: [
            trigger(
                'myAnimation',
                [
                  transition(
                      '* => 1, * => 2',
                      [
                        animate(1234, style({width: '100px'})),
                      ]),
                ]),
          ]
        })
        class Cmp {
          exp: any = false;
          disableExp = false;
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        fixture.detectChanges();
        resetLog();

        cmp.disableExp = true;
        cmp.exp = '1';
        fixture.detectChanges();

        let players = getLog();
        expect(players.length).toEqual(0);
        resetLog();

        cmp.disableExp = false;
        cmp.exp = '2';
        fixture.detectChanges();

        players = getLog();
        expect(players.length).toEqual(1);
        expect(players[0].totalTime).toEqual(1234);
      });

      it('should respect inner disabled nodes once a parent becomes enabled', () => {
        @Component({
          selector: 'if-cmp',
          template: `
              <div [@.disabled]="disableParentExp">
                <div [@.disabled]="disableChildExp">
                  <div [@myAnimation]="exp"></div>
                </div>
              </div>
            `,
          animations: [trigger(
              'myAnimation',
              [transition('* => 1, * => 2, * => 3', [animate(1234, style({width: '100px'}))])])]
        })
        class Cmp {
          disableParentExp = false;
          disableChildExp = false;
          exp = '';
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        fixture.detectChanges();
        resetLog();

        cmp.disableParentExp = true;
        cmp.disableChildExp = true;
        cmp.exp = '1';
        fixture.detectChanges();

        let players = getLog();
        expect(players.length).toEqual(0);

        cmp.disableParentExp = false;
        cmp.exp = '2';
        fixture.detectChanges();

        players = getLog();
        expect(players.length).toEqual(0);

        cmp.disableChildExp = false;
        cmp.exp = '3';
        fixture.detectChanges();

        players = getLog();
        expect(players.length).toEqual(1);
      });

      it('should properly handle dom operations when disabled', () => {
        @Component({
          selector: 'if-cmp',
          template: `
              <div [@.disabled]="disableExp" #parent>
                <div *ngIf="exp" @myAnimation></div>
              </div>
            `,
          animations: [
            trigger(
                'myAnimation',
                [
                  transition(
                      ':enter',
                      [
                        style({opacity: 0}),
                        animate(1234, style({opacity: 1})),
                      ]),
                  transition(
                      ':leave',
                      [
                        animate(1234, style({opacity: 0})),
                      ]),
                ]),
          ]
        })
        class Cmp {
          @ViewChild('parent') public parentElm: any;
          disableExp = false;
          exp = false;
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        cmp.disableExp = true;
        fixture.detectChanges();
        resetLog();

        const parent = cmp.parentElm!.nativeElement;

        cmp.exp = true;
        fixture.detectChanges();
        expect(getLog().length).toEqual(0);
        expect(parent.childElementCount).toEqual(1);

        cmp.exp = false;
        fixture.detectChanges();
        expect(getLog().length).toEqual(0);
        expect(parent.childElementCount).toEqual(0);
      });

      it('should properly resolve animation event listeners when disabled', fakeAsync(() => {
           @Component({
             selector: 'if-cmp',
             template: `
              <div [@.disabled]="disableExp">
                <div [@myAnimation]="exp" (@myAnimation.start)="startEvent=$event" (@myAnimation.done)="doneEvent=$event"></div>
              </div>
            `,
             animations: [
               trigger(
                   'myAnimation',
                   [
                     transition(
                         '* => 1, * => 2',
                         [style({opacity: 0}), animate(9876, style({opacity: 1}))]),
                   ]),
             ]
           })
           class Cmp {
             disableExp = false;
             exp = '';
             // TODO(issue/24571): remove '!'.
             startEvent!: AnimationEvent;
             // TODO(issue/24571): remove '!'.
             doneEvent!: AnimationEvent;
           }

           TestBed.configureTestingModule({declarations: [Cmp]});

           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;
           cmp.disableExp = true;
           fixture.detectChanges();
           resetLog();
           expect(cmp.startEvent).toBeFalsy();
           expect(cmp.doneEvent).toBeFalsy();

           cmp.exp = '1';
           fixture.detectChanges();
           flushMicrotasks();
           expect(cmp.startEvent.totalTime).toEqual(9876);
           expect(cmp.startEvent.disabled).toBeTruthy();
           expect(cmp.doneEvent.totalTime).toEqual(9876);
           expect(cmp.doneEvent.disabled).toBeTruthy();

           cmp.exp = '2';
           cmp.disableExp = false;
           fixture.detectChanges();
           flushMicrotasks();
           expect(cmp.startEvent.totalTime).toEqual(9876);
           expect(cmp.startEvent.disabled).toBeFalsy();
           // the done event isn't fired because it's an actual animation
         }));

      it('should work when there are no animations on the component handling the disable/enable flag',
         () => {
           @Component({
             selector: 'parent-cmp',
             template: `
              <div [@.disabled]="disableExp">
                <child-cmp #child></child-cmp>
              </div>
                `
           })
           class ParentCmp {
             @ViewChild('child') public child: ChildCmp|null = null;
             disableExp = false;
           }

           @Component({
             selector: 'child-cmp',
             template: `
                <div [@myAnimation]="exp"></div>
                `,
             animations: [trigger(
                 'myAnimation', [transition(
                                    '* => go, * => goAgain',
                                    [style({opacity: 0}), animate('1s', style({opacity: 1}))])])]
           })
           class ChildCmp {
             public exp = '';
           }

           TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

           const fixture = TestBed.createComponent(ParentCmp);
           const cmp = fixture.componentInstance;
           cmp.disableExp = true;
           fixture.detectChanges();
           resetLog();

           const child = cmp.child!;
           child.exp = 'go';
           fixture.detectChanges();

           expect(getLog().length).toEqual(0);
           resetLog();

           cmp.disableExp = false;
           child.exp = 'goAgain';
           fixture.detectChanges();
           expect(getLog().length).toEqual(1);
         });

      it('should treat the property as true when the expression is missing', () => {
        @Component({
          selector: 'parent-cmp',
          animations: [
            trigger(
                'myAnimation',
                [
                  transition(
                      '* => go',
                      [
                        style({opacity: 0}),
                        animate(500, style({opacity: 1})),
                      ]),
                ]),
          ],
          template: `
              <div @.disabled>
                <div [@myAnimation]="exp"></div>
              </div>
                `
        })
        class Cmp {
          exp = '';
        }

        TestBed.configureTestingModule({declarations: [Cmp]});

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        fixture.detectChanges();
        resetLog();

        cmp.exp = 'go';
        fixture.detectChanges();
        expect(getLog().length).toEqual(0);
      });

      it('should respect parent/sub animations when the respective area in the DOM is disabled',
         fakeAsync(() => {
           @Component({
             selector: 'parent-cmp',
             animations: [
               trigger(
                   'parent',
                   [
                     transition(
                         '* => empty',
                         [
                           style({opacity: 0}),
                           query(
                               '@child',
                               [
                                 animateChild(),
                               ]),
                           animate('1s', style({opacity: 1})),
                         ]),
                   ]),
               trigger(
                   'child',
                   [
                     transition(
                         ':leave',
                         [
                           animate('1s', style({opacity: 0})),
                         ]),
                   ]),
             ],
             template: `
              <div [@.disabled]="disableExp" #container>
                <div [@parent]="exp" (@parent.done)="onDone($event)">
                  <div class="item" *ngFor="let item of items" @child (@child.done)="onDone($event)"></div>
                </div>
              </div>
                `
           })
           class Cmp {
             @ViewChild('container') public container: any;

             disableExp = false;
             exp = '';
             items: any[] = [];
             doneLog: any[] = [];

             onDone(event: any) {
               this.doneLog.push(event);
             }
           }

           TestBed.configureTestingModule({declarations: [Cmp]});
           const engine = TestBed.inject(ɵAnimationEngine);
           const fixture = TestBed.createComponent(Cmp);
           const cmp = fixture.componentInstance;
           cmp.disableExp = true;
           cmp.items = [0, 1, 2, 3, 4];
           fixture.detectChanges();
           flushMicrotasks();

           cmp.exp = 'empty';
           cmp.items = [];
           cmp.doneLog = [];
           fixture.detectChanges();
           flushMicrotasks();

           const elms = cmp.container.nativeElement.querySelectorAll('.item');
           expect(elms.length).toEqual(0);

           expect(cmp.doneLog.length).toEqual(6);
         }));
    });
  });

  describe('animation normalization', () => {
    it('should convert hyphenated properties to camelcase by default', () => {
      @Component({
        selector: 'cmp',
        template: `
               <div [@myAnimation]="exp"></div>
             `,
        animations: [
          trigger(
              'myAnimation',
              [
                transition(
                    '* => go',
                    [
                      style({'background-color': 'red', height: '100px', fontSize: '100px'}),
                      animate(
                          '1s',
                          style({'background-color': 'blue', height: '200px', fontSize: '200px'})),
                    ]),
              ]),
        ]
      })
      class Cmp {
        exp: any = false;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = 'go';
      fixture.detectChanges();

      const players = getLog();
      expect(players.length).toEqual(1);
      expect(players[0].keyframes).toEqual([
        {backgroundColor: 'red', height: '100px', fontSize: '100px', offset: 0},
        {backgroundColor: 'blue', height: '200px', fontSize: '200px', offset: 1},
      ]);
    });

    it('should convert hyphenated properties to camelcase by default that are auto/pre style properties',
       () => {
         @Component({
           selector: 'cmp',
           template: `
               <div [@myAnimation]="exp"></div>
             `,
           animations: [
             trigger(
                 'myAnimation',
                 [
                   transition(
                       '* => go',
                       [
                         style({'background-color': AUTO_STYLE, 'font-size': '100px'}),
                         animate('1s', style({'background-color': 'blue', 'font-size': PRE_STYLE})),
                       ]),
                 ]),
           ]
         })
         class Cmp {
           exp: any = false;
         }

         TestBed.configureTestingModule({declarations: [Cmp]});
         const fixture = TestBed.createComponent(Cmp);
         const cmp = fixture.componentInstance;
         cmp.exp = 'go';
         fixture.detectChanges();

         const players = getLog();
         expect(players.length).toEqual(1);
         expect(players[0].keyframes).toEqual([
           {backgroundColor: AUTO_STYLE, fontSize: '100px', offset: 0},
           {backgroundColor: 'blue', fontSize: PRE_STYLE, offset: 1},
         ]);
       });
  });

  it('should throw neither state() or transition() are used inside of trigger()', () => {
    @Component({
      selector: 'if-cmp',
      template: `
          <div [@myAnimation]="exp"></div>
        `,
      animations: [trigger('myAnimation', [animate(1000, style({width: '100px'}))])]
    })
    class Cmp {
      exp: any = false;
    }

    TestBed.configureTestingModule({declarations: [Cmp]});

    expect(() => {
      TestBed.createComponent(Cmp);
    })
        .toThrowError(
            /only state\(\) and transition\(\) definitions can sit inside of a trigger\(\)/);
  });

  it('should combine multiple errors together into one exception when an animation fails to be built',
     () => {
       @Component({
         selector: 'if-cmp',
         template: `
          <div [@foo]="fooExp" [@bar]="barExp"></div>
        `,
         animations: [
           trigger(
               'foo',
               [
                 transition(':enter', []),
                 transition(
                     '* => *',
                     [
                       query('foo', animate(1000, style({background: 'red'}))),
                     ]),
               ]),
           trigger(
               'bar',
               [
                 transition(':enter', []),
                 transition(
                     '* => *',
                     [
                       query('bar', animate(1000, style({background: 'blue'}))),
                     ]),
               ]),
         ]
       })
       class Cmp {
         fooExp: any = false;
         barExp: any = false;
       }

       TestBed.configureTestingModule({declarations: [Cmp]});

       const engine = TestBed.inject(ɵAnimationEngine);
       const fixture = TestBed.createComponent(Cmp);
       const cmp = fixture.componentInstance;
       fixture.detectChanges();

       cmp.fooExp = 'go';
       cmp.barExp = 'go';

       let errorMsg: string = '';
       try {
         fixture.detectChanges();
       } catch (e) {
         errorMsg = e.message;
       }

       expect(errorMsg).toMatch(/@foo has failed due to:/);
       expect(errorMsg).toMatch(/`query\("foo"\)` returned zero elements/);
       expect(errorMsg).toMatch(/@bar has failed due to:/);
       expect(errorMsg).toMatch(/`query\("bar"\)` returned zero elements/);
     });

  it('should not throw an error if styles overlap in separate transitions', () => {
    @Component({
      selector: 'if-cmp',
      template: `
          <div [@myAnimation]="exp"></div>
        `,
      animations: [
        trigger(
            'myAnimation',
            [
              transition(
                  'void => *',
                  [
                    style({opacity: 0}),
                    animate('0.5s 1s', style({opacity: 1})),
                  ]),
              transition(
                  '* => void',
                  [animate(1000, style({height: 0})), animate(1000, style({opacity: 0}))]),
            ]),
      ]
    })
    class Cmp {
      exp: any = false;
    }

    TestBed.configureTestingModule({declarations: [Cmp]});

    expect(() => {
      TestBed.createComponent(Cmp);
    }).not.toThrowError();
  });

  modifiedInIvy('FW-952 - Error recovery is handled differently in Ivy than VE')
      .it('should continue to clean up DOM-related animation artifacts even if a compiler-level error is thrown midway',
          () => {
            @Component({
              selector: 'if-cmp',
              animations: [
                trigger(
                    'foo',
                    [
                      transition('* => something', []),
                    ]),
              ],
              template: `
          value = {{ foo[bar] }}
          <div #contents>
            <div *ngIf="exp">1</div>
            <div *ngIf="exp" @foo>2</div>
            <div *ngIf="exp" [@foo]="'123'">3</div>
          </div>
        `,
            })
            class Cmp {
              exp: any = false;

              @ViewChild('contents', {static: true}) public contents: any;
            }

            TestBed.configureTestingModule({declarations: [Cmp]});

            const engine = TestBed.inject(ɵAnimationEngine);
            const fixture = TestBed.createComponent(Cmp);

            const runCD = () => fixture.detectChanges();
            const cmp = fixture.componentInstance;

            cmp.exp = true;
            expect(runCD).toThrow();

            const contents = cmp.contents.nativeElement;
            expect(contents.innerText.replace(/\s+/gm, '')).toEqual('123');

            cmp.exp = false;
            expect(runCD).toThrow();

            expect(contents.innerText.trim()).toEqual('');
          });

  describe('errors for not using the animation module', () => {
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [{provide: RendererFactory2, useExisting: ɵDomRendererFactory2}],
      });
    });

    it('should throw when using an @prop binding without the animation module', () => {
      @Component({template: `<div [@myAnimation]="true"></div>`})
      class Cmp {
      }

      TestBed.configureTestingModule({declarations: [Cmp]});
      const comp = TestBed.createComponent(Cmp);
      expect(() => comp.detectChanges())
          .toThrowError(
              'Found the synthetic property @myAnimation. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.');
    });

    it('should throw when using an @prop listener without the animation module', () => {
      @Component({template: `<div (@myAnimation.start)="a = true"></div>`})
      class Cmp {
        a: any;
      }

      TestBed.configureTestingModule({declarations: [Cmp]});

      expect(() => TestBed.createComponent(Cmp))
          .toThrowError(
              'Found the synthetic listener @myAnimation.start. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.');
    });
  });
});
})();

function assertHasParent(element: any, yes: boolean) {
  const parent = element.parentNode;
  if (yes) {
    expect(parent).toBeTruthy();
  } else {
    expect(parent).toBeFalsy();
  }
}

function buildParams(params: {[name: string]: any}): AnimationOptions {
  return {params};
}
