Understanding Animation Testing in Angular

Animations play a pivotal role in the modern web landscape, elevating user interaction and infusing interfaces with a lively, responsive feel. Angular comes equipped with a powerful animation framework that enables the creation of smooth, visually appealing state changes. Yet, verifying that these animations function as intended through rigorous testing is just as vital as crafting them.

Validating animations within an Angular project can pose difficulties, but it is a necessary endeavor for developers who aim to ship polished, dependable user experiences. Throughout this detailed exploration, we will cover a range of methodologies and tactics for testing animations effectively within the Angular ecosystem. Whether it’s performing isolated tests on components or confirming the correct application of animation keyframes, we’ll examine the utilities and recommended approaches that will enable you to construct strong, trustworthy animations.

Regardless of your experience level—whether you are an experienced Angular specialist or a newcomer to front-end development—grasping animation testing will notably enhance the robustness and maintainability of your applications. We’ll investigate the nuances of animation testing in Angular and advance your development practices.

Foundation: Setting Up a Simple Animation in Angular

Begin by generating a fresh Angular project featuring a straightforward open/close animation, which will be initiated through a button interaction. The project uses Angular version 17.3.0, with default settings retained during setup.

ng new angular-animation-testing
cd angular-animation-testing
Enter fullscreen mode Exit fullscreen mode

To keep things organized, animations are stored in a dedicated file. Create a new file for this purpose.

// src/animations/open-close.animation.ts
import {
  animate,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';

export const openCloseAnimation = trigger('openClose', [
  state(
    'open',
    style({
      height: '200px',
      opacity: 1,
      backgroundColor: 'yellow',
    })
  ),
  state(
    'closed',
    style({
      height: '100px',
      opacity: 0.8,
      backgroundColor: 'blue',
    })
  ),
  transition('open => closed', [animate('1s')]),
  transition('closed => open', [animate('0.5s')]),
  transition('* => closed', [animate('1s')]),
  transition('* => open', [animate('0.5s')]),
  transition('open <=> closed', [animate('0.5s')]),
  transition('* => open', [animate('1s', style({ opacity: '*' }))]),
  transition('* => *', [animate('1s')]),
]);
Enter fullscreen mode Exit fullscreen mode

Modify your app.component.ts, adding the new animation to the animations array. The component should resemble the following:

// app.component.ts
import { Component } from '@angular/core';
import { openCloseAnimation } from './animations/open-close.animation';
@Component({
  selector: 'app-root',
  standalone: true,

  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  animations: [openCloseAnimation],
})
export class AppComponent {
  isOpen = true;

  toggle() {
    this.isOpen = !this.isOpen;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the template HTML file, insert a div element and bind the animation to it:

<!-- src/app/app.component.html -->
<button type="button" (click)="toggle()">Toggle Open/Close</button>

<div [@openClose]="isOpen ? 'open' : 'closed'"
  class="open-close-container">
  <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Launch the application via npm start to verify the animation operates correctly. The expected output should appear as shown:

Open Closed Animation

Unit testing fundamentals

With a basic animation ready for testing, we can explore several test scenarios. Let's begin by configuring our spec file. In this walkthrough, I'll stick with the standard TestBed configuration. Personally, I prefer SIFERS combined with ATL, and I've written a separate article on that approach if you're interested.

Test configuration

Start by bringing in the required modules and components in your test file:

import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
Enter fullscreen mode Exit fullscreen mode

Configure the beforeEach block with the essential modules. Since we're dealing with standalone components, the AppComponent goes into the imports array rather than the conventional declarations array. I've also made fixture and animatedElement public properties so they're readily accessible across all tests.

// app.component.spec.ts
let fixture:ComponentFixture<AppComponent>;
let animatedElement: HTMLDivElement;

beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [AppComponent, BrowserAnimationsModule],
  }).compileComponents();

  fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const element = fixture.nativeElement;
  animatedElement = element.querySelector('div');
});
Enter fullscreen mode Exit fullscreen mode

With this setup in place, we can write our initial unit test.

// app.component.spec.ts
it('should check the styles when the div is open', () => {
  const computedStyle = getComputedStyle(animatedElement);

  expect(computedStyle.opacity).toBe('1');
  expect(computedStyle.height).toBe('200px');
  expect(computedStyle.backgroundColor).toBe('rgb(255, 255, 0)');
});
Enter fullscreen mode Exit fullscreen mode

When you execute this through ng run test, it should pass without issues.

Karma

Let's add some debugging to see what's actually happening. The real animation is firing 🤯. This occurs because the BrowserAnimationModule has been imported. Testing against genuine animations isn't advisable.

Animation Execution

Avoid testing real animation implementations

Performance and reliability concerns

  • Execution Time: Genuine animations can dramatically slow down test runs, especially when you have a large number of animation-related tests. Your overall test suite duration can suffer significantly.
  • Cross-Browser Variability: Animation performance and behavior can differ across browsers and their versions. This inconsistency makes it challenging to write dependable tests that pass uniformly across all environments.
  • Reliance on Visual Inspection: Tests that depend on what you see are subjective and can be error-prone. A subtle visual discrepancy could indicate a real bug or merely a harmless rendering difference.

Control and Isolation challenges

  • Focus on Logic, not Visuals: The core objective is to verify the component's logic that triggers animations and manages state transitions, not to assess the visual outcome.
  • Difficult to Mock Events: Reproducing specific animation states or timing sequences becomes cumbersome when you depend on real animations and genuine user interactions.

Introducing MockAnimationDriver

Angular provides the MockAnimationDriver for this exact purpose (pun partially intended). This mock replaces the entire Angular animation driver, giving you finer control over animation behavior. It's particularly useful for complex scenarios where you need to simulate animation execution or verify applied animation styles.

Internally, it relies on the MockAnimationPlayer, which emulates the behavior of an animation player tied to a specific animation trigger. This is handy for checking how your component responds to animation state changes and confirming which triggers are used. The player exposes several useful methods for testing:

Check out the complete method list here.

One thing to keep in mind: MockAnimationDriver inherits from NoopAnimationPlayer, so certain methods are noop and perform no action. Remember this when your tests seem to do nothing.

Adjusting the testing module

Your TestBed configuration needs to provide the MockAnimationDriver. This time, I've also included NoopAnimationModule, which disables animations during testing, giving us full control via MockAnimationDriver.

// app.component-2.spec.ts
beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [AppComponent, NoopAnimationsModule],
    providers: [{
      provide: AnimationDriver, 
      useClass: MockAnimationDriver
    }],
  }).compileComponents();
});
Enter fullscreen mode Exit fullscreen mode

I've also added an extra beforeEach to grab component references:

// app.component2.spec.ts
beforeEach(() => {
  fixture = TestBed.createComponent(AppComponent);
  component = fixture.componentInstance;

  button = fixture.debugElement
    .query(By.css('button'))
    .nativeElement;

  openCloseContainer = fixture.debugElement
    .query(By.css('.open-close-container'))
    .nativeElement;
});
Enter fullscreen mode Exit fullscreen mode
  • Using the fixture's debug tools, I obtained references to the specific elements in the template:
    • component: This provides the instance of AppComponent.
    • button: The button element responsible for toggling the animation.
    • openCloseContainer: The element whose styles are directly affected by the animation.

Writing Tests for the Animations

Now for the engaging part — composing tests to confirm that the animations behave as intended.

Test 1. Confirming the initial animation:

The component under test uses a button to flip a box between the open and closed states. Upon initial render, the default transition * => open fires. This notation indicates moving from any starting state into the open state. Additional details on this syntax can be found here.

// app.component2.spec.ts
it('should start "* => open" animation on the first load', () => {
  fixture.detectChanges();

  let player = MockAnimationDriver.log.pop()! as MockAnimationPlayer;

  expect(player.keyframes).toEqual([
    new Map<string, string|number>([[ 'height', '*' ], [ 'opacity', '*' ], [ 'backgroundColor', '*' ], [ 'offset', 0 ]]),
    new Map<string, string|number>([[ 'height', '200px' ], [ 'opacity', '1' ], [ 'backgroundColor', 'yellow' ], [ 'offset', 1 ]]),
  ]);

  // We are still animating
  expect(openCloseContainer.classList.contains('ng-animating')).toBeTruthy();

  player.finish();

  // We are done with the animation
  expect(openCloseContainer.classList.contains('ng-animating')).toBeFalsy();

  const computedStyle = window.getComputedStyle(openCloseContainer);
  expect(computedStyle.backgroundColor).toBe('rgb(255, 255, 0)'); // yellow
});
Enter fullscreen mode Exit fullscreen mode

Detailed Walkthrough:

  1. Forcing Change Detection: The fixture.detectChanges() call ensures that any data bindings or internal component state are synchronized into the DOM. This step is indispensable since the animation may depend on the component's startup configuration.
  2. Retrieving the Animation Player: The line let player = MockAnimationDriver.log.pop()! as MockAnimationPlayer; pulls the animation player responsible for the initial transition from the MockAnimationDriver.log.pop! stack. The as MockAnimationPlayer cast assures the fetched item is handled as a MockAnimationPlayer instance.
  3. Checking Keyframes: The statement expect(player.keyframes).toEqual(...) evaluates the player's actual keyframes against the expected set. These keyframes define the boundaries of the animation for attributes like height, opacity, and backgroundColor. In this scenario, the element begins at arbitrary height and opacity values (*) and settles at a height of 200px, full opacity (1), and a yellow background.
  4. Verifying Active Animation: The check expect(openCloseContainer.classList.contains('ng-animating')).toBeTruthy(); confirms that the openCloseContainer element carries the ng-animating class. Angular applies this class while an animation is in progress, so this assertion confirms the animation is still running after the keyframe validation.
  5. Finishing the Animation: Calling player.finish(); forces the animation to jump to its final state. This is required because the test must verify the resulting styles after the animation concludes.
  6. Confirming Animation Completion: The assertion expect(openCloseContainer.classList.contains('ng-animating')).toBeFalsy(); rechecks for the ng-animating class on the openCloseContainer element. This time, it should be absent, as the animation should have concluded following player.finish().
  7. Validating Final Styles: The expression const computedStyle = window.getComputedStyle(openCloseContainer); accesses the computed styles of the openCloseContainer element. The check expect(computedStyle.backgroundColor).toBe('rgb(255, 255, 0)'); confirms that the element's final background color is yellow, matching the values specified in the animation keyframes.

In essence, this test verifies that:

  • The appropriate animation runs during startup (* => open).
  • The animation keyframes align with the intended behavior.
  • The animation visibly transitions the element into its open state, featuring a yellow background.

Test 2. Confirming the open => closed animation

A second test can be authored in a similar fashion to simulate the open => closed transition. It mirrors the prior steps, with the exception of adding a button click and omitting the initial fixture.detectChanges invocation. The test appears as follows:

// app.component2.spec.ts
it('should start "open => closed" animation when toggled to closed', () => {
  button.click();
  fixture.detectChanges();

  let player = MockAnimationDriver.log.pop()! as MockAnimationPlayer;

  expect(player.keyframes).toEqual([
    new Map<string, string|number>([[ 'height', '*' ], [ 'opacity', '*' ], [ 'backgroundColor', '*' ], [ 'offset', 0 ] ]),
    new Map<string, string|number>([[ 'height', '100px' ], [ 'opacity', '0.8' ], [ 'backgroundColor', 'blue' ], [ 'offset', 1 ]]),
  ]);

  // We are still animating
  expect(openCloseContainer.classList.contains('ng-animating')).toBeTruthy();

  player.finish();

  // We are done with the animation
  expect(openCloseContainer.classList.contains('ng-animating')).toBeFalsy();

  const computedStyle = window.getComputedStyle(openCloseContainer);
  expect(computedStyle.backgroundColor).toBe('rgb(0, 0, 255)'); // blue
});
Enter fullscreen mode Exit fullscreen mode

Detailed Walkthrough:

  1. Initiating the Toggle: The test starts the animation by invoking button.click();, which is presumed to set off the closed state.
  2. Validating Keyframes for the Closed State: The expected keyframes correspond to the shift into a closed state:
    • Height: 100px
    • Opacity: 0.8
    • Background color: blue
  3. Finalizing the Animation and Evaluating Styles: This test follows the same trajectory as the first, using player.finish(); to complete the animation and then checking the final computed styles, confirming the background color is now blue to indicate the closed state.

Important Considerations:

This test verifies that the appropriate animation executes when toggling to the closed state. It confirms that the keyframes match the anticipated values for the closed state and that the element visually transitions into that closed state, with a blue background. Collectively, these two tests thoroughly cover the animation behavior for both the open and closed states, guaranteeing a seamless and visually pleasing interaction.

Final Thoughts

In summary, rigorously testing animations in Angular is vital for sustaining a polished and intuitive application. By employing MockAnimationDriver along with the strategies described here, you can craft unit tests that validate animation logic, keyframes, and visual results without depending on actual animation execution. This method speeds up test runs, sidesteps browser variations, and encourages a more resilient development approach.

Additionally, MockAnimationDriver facilitates the testing of intricate animation scenarios involving multiple triggers and timings. You can replicate specific animation states and timings to confirm that your component correctly interacts with the animation lifecycle. While unit tests concentrate on component logic and animation behavior, consider adding visual regression testing tools for extra confidence in the visual precision of animations across various platforms. Finally, incorporate animation tests into your continuous integration workflow to catch regressions early in the development cycle. By adhering to these practices, you can guarantee that your Angular animations execute flawlessly and provide an attractive user experience.

And lastly, here is the full code referenced throughout this article.

Github: https://github.com/sonukapoor/animation-testing


👋 Let's Connect!

If this article was helpful, feel free to reach out:

🔗 Follow me on LinkedIn
💻 Check out my GitHub
Buy me a coffee