Testing Animations in Angular: A Comprehensive Guide

sonukapoor

Sonu Kapoor

Posted on April 14, 2024

Testing Animations in Angular: A Comprehensive Guide

Introduction

Animations are a crucial aspect of modern web applications, enhancing user experience and adding a dynamic touch to the interface. Angular provides a robust animation system that allows developers to create fluid and engaging UI transitions. However, ensuring the reliability and correctness of these animations through testing is equally important.

Testing animations in Angular can be a challenging yet essential task for developers aiming to deliver a polished and bug-free user experience. In this comprehensive guide, we will explore various strategies and techniques to effectively test animations within the Angular framework. From unit testing individual components to validating the application of keyframes, we'll dive into the tools and best practices that will empower you to build robust and reliable animations.

Whether you are a seasoned Angular developer or just starting your journey into web development, understanding how to test animations will contribute significantly to the overall quality and maintainability of your Angular applications. Let's take a look at the ins and outs of animation testing in Angular and elevate your development workflow to the next level.

Prerequisite: Creating a Basic Animation in Angular

Let's first create a new Angular project with a very simple open/close animation that will be triggered on a button click. The Angular version used in this article is 17.3.0. I will keep the standard options while creating the project.

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

I keep my animations in a separate file. Go ahead and create a new file for that.

// 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

Open your app.component.ts and add the new animation to your animations array. Your component should look like this:

// 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

Open the template html file and add a div and apply the animation to that element:

<!-- 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

Run the application, using npm start to ensure that your animation is working. You should see something like this:

Open Closed Animation

Unit testing

Now that we have a basic animation ready to tested, let's look at some testing scenarios. Let's start with the setup of our spec file. For this article, I will use the traditional way to set up my TestBed. I usually use SIFERS along with ATL. I wrote a separate article on this if you would like to read it.

Setup

Import the necessary modules/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

Let's set up the beforeEach with the essential modules. Since I am using standalone components, I need to add the AppComponent to the imports array, instead of the usual declarations array. I have also created the fixture and the animatedElement as public variables so that I can easily access them in my 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 the above setup, let's create our first 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

Run this test using ng run test and you will see that it passes.

Karma

Now, let's debug this test and see what happens. It triggered the real animation 🤯. That is happening because we are importing the BrowserAnimationModule. It is not recommended to test the real animation.

Animation Execution

Do not test real animation implementations

Slowness and Inconsistencies:

  • Execution Time: Running real animations can be slow, especially during test suites with many animation tests. This can significantly increase your test execution time.
  • Browser Inconsistencies: Animation behaviour can vary slightly across different browsers and even browser versions. This variability makes it difficult to write reliable tests that pass consistently everywhere.
  • Visual Dependence: Tests relying on visual inspection are subjective and prone to errors. A small visual difference might be a bug or just a minor rendering inconsistency.

Limited Control and Isolation:

  • Focus on Logic, not Visuals: Your primary concern is testing the component's logic related to triggering animations and handling state changes, not the visual outcome.
  • Difficult to Mock Events: Testing specific animation states or timings becomes cumbersome when relying on real animations and user interaction.

MockAnimationDriver to the Rescue

Here's where MockAnimationDriver from Angular comes into play (pun intended). This mock object replaces the entire Angular animation driver, allowing you to control animation behaviour at a more granular level. It helps test complex scenarios where you need to simulate animation execution or specific animation styles.

Under the hood, the MockAnimationPlayer is used, which simulates the behaviour of an animation player for a specific animation trigger. It's useful for testing how your component interacts with animation state changes and verifying the used animation triggers. The player has several methods, that can help you in testing.

See the full list here

The MockAnimationDriver extends NoopAnimationPlayer and because of that some of the methods are noop, and don't do anything. Keep this in mind when writing your tests and you are wondering why nothing is happening.

Setting Up the Testing Module

The MockAnimationDriver needs to be provided in our TestBed setup. This time I have also imported the NoopAnimationModule. NoopAnimationsModule which disables animations by default in testing, allowing us to control them with 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 also added another beforeEach to get 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
  • I utilized the fixture's debugging tools to obtain references to specific elements in the component's template: component:
    • This holds the instance of AppComponent.
    • button: This references the button element that triggers the animation toggle.
    • openCloseContainer: This references the element whose styles are affected by the animation.

Writing Tests for Animations

Now comes the exciting part - writing tests to verify the animations!

Test 1. Verifying initial animation:

Our component has a button that toggles a box between the on and closed event. When the component loads, the initial animation * => open will trigger. This syntax translates to animate from any state to open. You can learn more about this syntax 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

Step-by-Step Breakdown:

  1. Triggering Change Detection: fixture.detectChanges() ensures that any data binding or changes within the component are reflected in the DOM. This is crucial because the animation might be triggered based on the component's initial state.
  2. Accessing the Animation Player: let player = MockAnimationDriver.log.pop()! as MockAnimationPlayer; retrieves the animation player created for the initial animation using MockAnimationDriver.log.pop!. The as MockAnimationPlayer type assertion ensures the retrieved value is treated as a MockAnimationPlayer.
  3. Verifying Animation Keyframes: expect(player.keyframes).toEqual(...) compares the actual animation keyframes of the retrieved player with the expected keyframes. These keyframes define the animation's starting and ending states for various properties like height, opacity, and backgroundColor. In this case, the animation starts from any height and opacity values (*) and transitions to a height of 200px, full opacity (1), and a yellow background color.
  4. Checking for Ongoing Animation: expect(openCloseContainer.classList.contains('ng-animating')).toBeTruthy(); verifies that the openCloseContainer element has the ng-animating class added to its class list. Angular adds this class during animation playback. This assertion confirms that the animation is still ongoing after the keyframe verification.
  5. Simulating Animation Completion: player.finish(); simulates the animation reaching its end state. This is necessary because the test needs to verify the final styles after the animation finishes.
  6. Checking for Animation Completion (again): expect(openCloseContainer.classList.contains('ng-animating')).toBeFalsy(); checks once more if the ng-animating class is present on the openCloseContainer element. This time, we expect it to be not there because the animation should be finished after calling player.finish().
  7. Verifying Final Styles: const computedStyle = window.getComputedStyle(openCloseContainer); retrieves the computed styles of the openCloseContainer element. expect(computedStyle.backgroundColor).toBe('rgb(255, 255, 0)'); asserts that the final background color of the element is yellow (as defined in the animation keyframes).

Overall, this test effectively verifies that:

  • The correct animation plays on initial load (* => open).
  • The animation keyframes match the expected behavior.
  • The animation visually transitions the element to its open state (yellow background).

Test 2. Verifying the open => closed animation

Similarly we can write a second test, that simulates the open => closed state. It follows the same steps, except with the additional button click and the removal of the initial fixture.detectChanges call. Here is how this test would look:

// 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

Step-by-Step Breakdown:

  1. Triggering the Toggle: The test initiates the animation by clicking a button using button.click();, assuming it triggers the closed state.
  2. Verifying Keyframes for Closed State: The expected keyframes match the transition to a closed state:
    • Height: 100px
    • Opacity: 0.8
    • Background color: blue
  3. Simulating Animation Completion and Final Styles: The test follows a similar pattern as the first test, simulating animation completion with player.finish(); and asserting the final styles: Background color is now blue, confirming the closed state.

Key Points:

This test verifies the correct animation plays when toggling to the closed state. It ensures the animation keyframes align with the expected closed state values. It validates that the element visually transitions to the closed state (blue background). Together, these two tests comprehensively cover the animation behavior for both open and closed states, ensuring a smooth and visually appealing user experience.

Conclusion

In conclusion, effectively testing animations in Angular is crucial for maintaining a polished and user-friendly application. By leveraging MockAnimationDriver and the techniques outlined in this guide, you can write unit tests that verify animation logic, keyframes, and visual outcomes without relying on real animation execution. This approach ensures faster test execution times, avoids browser inconsistencies, and promotes a more robust development workflow.

Furthermore, MockAnimationDriver empowers testing complex animation scenarios with multiple triggers and timings. You can simulate specific animation states and timings to ensure your component interacts correctly with the animation lifecycle. While unit tests focus on component logic and animation behavior, consider incorporating visual regression testing tools for additional reassurance about the visual fidelity of animations across different environments. Finally, integrate animation tests into your continuous integration pipeline to catch regressions early on in the development process. By following these practices, you can ensure that your Angular animations function flawlessly and deliver a visually appealing user experience.

And finally, here is the code discussed in this article.

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

💖 💪 🙅 🚩
sonukapoor
Sonu Kapoor

Posted on April 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related