Search code examples
javascriptangularjestjsrxjs

Angular - How to unit test an RxJS timer used with AsyncPipe


I am unsuccessful in writing a unit test for an RxJS timer that is displayed in the template with an AsyncPipe.

Component:

import { Component } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { map, timer } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    Timer should fire after {{ delay }} seconds:
    <ng-container *ngIf="timer$ | async as timer; else waiting">Timer fired {{ timer }} times</ng-container>
    <ng-template #waiting>Waiting for Timer…</ng-template>
  `,
  imports: [AsyncPipe, NgIf],
})
export class AppComponent {
  delay = 3;
  timer$ = timer(this.delay * 1000, 1000).pipe(map((n) => n + 1));
}

Test:

import { AppComponent } from './main';
import {
  ComponentFixture,
  TestBed,
} from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

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

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should initially render waiting', () => {
    expect(fixture.nativeElement.textContent).toContain('Waiting'); //passes
  });

  it('should emit timer', async () => {
    expect(await firstValueFrom(component.timer$)).toBe(1); //passes
  });

  it('should show "Timer fired"', () => {
    jest.useFakeTimers();
    jest.advanceTimersByTime(5000);

    expect(fixture.nativeElement.textContent).toContain('Timer fired'); //fails
  });
});

Stackblitz: https://stackblitz.com/edit/angular-rxjs-timer-asyncpipe-unit-test?file=src%2Fmain.ts,src%2Ftest.spec.ts

The most similar question here on StackOverflow is this one: Angular testing async pipe does not trigger the observable Unfortunately, the solution described in the most upvoted answer does not solve the problem.


Solution

  • We can use fakeAsync and tick of @angular/core/testing to simulate the time lapse.

    Then we can use fixture.whenStable() to check the view.

    it('should show "Timer fired"', fakeAsync((done: jest.DoneCallback) => {
      expect.assertions(1);
      tick(3000);
      fixture.whenStable().then(() => {
        expect(fixture.nativeElement.textContent).toContain('Timer fired');
        done();
      });
    })); 
    

    The same test rewritten with async/await seems to be the exact same but does not work:

    it('should show "Timer fired"', fakeAsync(async () => {
      tick(3000);
      await fixture.whenStable();
      expect(fixture.nativeElement.textContent).toContain('Timer fired');
    })); 
    

    Stackblitz Demo