Issue with Angular Testing: Tick function fails to work properly when component initialization includes a timer

Question

How can I make the `tick` function work properly so that my test advances by 10s and calls `submit` in my component as expected?

Note: I am looking for a solution other than using

await new Promise(r => setTimeout(r, 10000))
to avoid having lengthy tests.

Goal

The objective is to ensure that `submit` only triggers `cb` after 10 seconds have passed since the component was created.

Description

Within the component, there is a timer set for 10s. Once this timer elapses, it changes a subject from false to true, indicating whether it is "valid" to submit data within the component.

In testing, the `tick` function does not appear to progress the timer, leading to the full 10s duration. Attempts were made to use `fakeAsync` in the `beforeEach` section when creating the component but without success.

What I have tried

  • Using `fakeAsync` in both the test component initialization and during the actual test
  • Using `setTimeout(() => this.obs.next(true), 10_000)` instead of the timer
  • Replacing the timer with `empty().pipe(delay(10000)).subscribe(() => this.obs.next(true));`
  • Moving the `timer` logic from `ngOnInit` to the constructor and vice versa

Observations

An adjustment in the code:

    timer(10_000).subscribe(() => {
      debugger;
      this.testThis$.next(true)
    });

reveals that upon running a test, the JavaScript debugger in Dev Tools activates 10 seconds after the component creation rather than immediately if `tick` functionality worked correctly.

Code

Shown below are the relevant snippets of code. A link to a minimal reproduction on GitHub can be found at the end.

// Component Code
import { Component, OnInit, Inject } from '@angular/core';
import { BehaviorSubject, Subject, timer } from 'rxjs';
import { first, filter } from 'rxjs/operators';

@Component({
  selector: 'app-tick-test',
  templateUrl: './tick-test.component.html',
  styleUrls: ['./tick-test.component.scss']
})
export class TickTestComponent implements OnInit {

  public testThis$: Subject<boolean>;

  constructor(
    @Inject('TICK_CALLBACK') private readonly cb: () => void,
  ) {
    this.testThis$ = new BehaviorSubject<boolean>(false);
    timer(10_000).subscribe(() => this.testThis$.next(true));
  }

  public ngOnInit(): void {
  }

  public submit(): void {
    // Execute the callback after 10s
    this.testThis$
      .pipe(first(), filter(a => !!a))
      .subscribe(() => this.cb());
  }
}
// Test Code
/**
 * The issue here revolves around expecting `tick` to advance
 * the timer initiated in the constructor, which is currently not functioning
 */

import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { TickTestComponent } from './tick-test.component';

describe('TickTestComponent', () => {
  let component: TickTestComponent;
  let fixture: ComponentFixture<TickTestComponent>;
  let callback: jasmine.Spy;

  beforeEach(async(() => {
    callback = jasmine.createSpy('TICK_CALLBACK');

    TestBed.configureTestingModule({
      providers: [
        { provide: 'TICK_CALLBACK', useValue: callback },
      ],
      declarations: [ TickTestComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TickTestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should become true after 10s', fakeAsync(() => {
    tick(10_001);

    component.submit();
    expect(callback).toHaveBeenCalled();
  }));
});

Minimal Reproduction Repository

GitHub Link for Minimal Reproduction

Answer №1

Solution

  1. Make sure to include fixture.detectChanges() in each test and perform tick(10_000) within those tests.
  2. Transfer the timer(10_000)... function to the ngOnInit method of the component.

Explanation

When utilizing fakeAsync, a specific "zone" is created for your code execution. This zone persists until it goes out of scope. Placing fakeAsync in the beforeEach disrupts the zone, resulting in timer issues where the timer does not complete as intended.

To address this, move the timer logic to the ngOnInit method since it is not executed immediately upon calling .createComponent. Instead, it is triggered when the first fixture.detectChanges() is called. Therefore, by invoking fixture.detectChanges() within the fakeAsync zone of a test for the first time, ngOnInit is automatically invoked, allowing proper time control.

Code Snippet

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

  let callback: jasmine.Spy;

  beforeEach(async(() => {

    callback = jasmine.createSpy('TICK_CALLBACK');

    TestBed.configureTestingModule({
      providers: [
        { provide: 'TICK_CALLBACK', useValue: callback },
      ],
      declarations: [ TickTestComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TickTestComponent);
    component = fixture.componentInstance;
    // do not execute here
    // fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should return true after 10 seconds', fakeAsync(() => {
    // if this is the initial detectChanges call, ngOnInit will be invoked
    fixture.detectChanges();
    tick(10_001);

    component.submit();
    expect(callback).toHaveBeenCalled();
  }));
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The Microsoft.Azure.WebJobs.Script encountered an issue while attempting to cast an object of type 'System.String' to type 'Microsoft.AspNetCore.Http.HttpRequest' during the return process

I recently encountered an issue with my Azure Function written in JS that is triggered by the Service Bus and generates files to Blob Storage. When attempting to return an HTTP result, I received the following error message: System.Private.CoreLib: Except ...

Displaying server errors in an Angular componentIn this tutorial, we

As I work on creating a registration page, my focus has been on posting data to the server. I have successfully implemented client-side and server-side validation mechanisms. Managing client-side errors is straightforward using code such as *ngIf="(emailAd ...

Ways to initiate a language shift in the **JavaScript yearly calendar** when the language is modified in an Angular application

1. I have incorporated ngx-translate into my Angular application for translation purposes. 2. I have successfully implemented the ability to switch languages during initialization by specifying options like {language:'ja'} for Japanese within th ...

Adding query parameters to links in Angular 10: A beginner's guide

I'm trying to update a link: <a class="contact-email" href="mailto:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="117268737463656364727a51666370617c7065743f727e7c">[email protected]</a>" ...

The router is unable to direct when an item is clicked

I am encountering an issue with my routing setup - when I click on an item in the list-item component, it does not successfully route to the detail-component. Here is a glimpse of my source code: product-list.component.html: <h1>Product List Compon ...

Using the amDateFormat pipe in Ionic 3's Lazy Loading feature

Currently, I am using Ionic3 and working on transitioning to Lazy Loading to enhance the startup performance. Since converting my ChatsPage to lazy loading, I have encountered an issue with pipes. The error message points to a specific line in my chats.ht ...

Using ngIf to locate a substring

<ul class="list-group" *ngFor="let head of channelDisplayHeads"> <li class="list-group-item" *ngFor="let channel of channelList" ngIf="channel.channel.indexOf('head') === 1"> <strong>{{ head }}</strong> ...

When conducting unit testing in Angular, it is important to avoid calling the Angular service if the return type is an observable of any when dealing

Using the Spyon function to mock a method call on a service and return an observable of 'TEST'. This method is for a service. Although similar methods with a class object return type are working fine in the debugger, there seems to be an issue wi ...

Utilizing the '+' symbol in path for efficient Express routing

Currently, I am utilizing Express to manage the hosting of my Angular2 application on Azure cloud services. In accordance with the Angular2 style guide, I have designated certain components as lazy loaded by adding a '+' prefix to their folder n ...

What is the best way to utilize imported classes, functions, and variables within an Angular 2 template?

I've come up with a solution for incorporating static content into a template, but I'm unsure if it's the best approach. I would like to know if there is an official or more efficient method of achieving this. Here's an example showcas ...

The TypeScript Mongoose function is not available for type <Context = any>

How can I implement a schema method in Mongoose and TypeScript to compare passwords? interface IUser extends Document { email: string; username: string; password: string; role: string; comparePassword( candidatePassword: string, next: Nex ...

Visual Verification

I'm currently working on a NestJS application that serves images with authentication requirements. I have implemented JWT for authentication, but I encountered an issue when trying to display the image in an img tag because I cannot attach the Authori ...

Arrange the items in the last row of the flex layout with equal spacing between them

How can I arrange items in rows with equal space between them, without a large gap in the last row? <div fxFlex="row wrap" fxLayoutAlign="space-around"> <my-item *ngFor="let item of items"></my-item> </div> Current Issue: htt ...

Using ngrx store select subscribe exclusively for designated actions

Is it possible to filter a store.select subscription by actions, similar to how we do with Effects? Here is an example of the code in question: this.store .select(mySelector) .subscribe(obj => { //FILTER SUBSCRIPTION BY ACTION this.object = ob ...

A guide on setting values within a FormBuilder array

I am facing an issue with setting array values in formBuilder.array. While it is easy to set values in a regular formBuilder, I am struggling to do the same in formBuilder.array. I have tried setting values for both 'listServiceFeature' and &apos ...

Using object in Typescript for function overloading - External visibility of implementation signatures for overloads is restricted

Issue How do I correctly expose an overloaded implementation signature? Scenario Expanding on the initial query: interface MyMap<T> { [id: string]: T; } type Options = { asObject?: boolean, other?: Function testing?: number }; function g ...

Ways to retrieve the component name from a service in Angular without relying on private APIs such as view container refs

How can I access the component name inside a service that is calling a method within the service? I have tried using console.log(this.vcr['_view'].component) and console.log(this.vcr['_view'].component.constructor.name), but they do not ...

lint-staged executes various commands based on the specific folder

Within my project folder, I have organized the structure with two subfolders: frontend and backend to contain their respective codebases. Here is how the root folder is set up: - backend - package.json - other backend code files - frontend - p ...

A different component experiences an issue where Angular Promise is returning undefined

This is the carComponent.ts file containing the following method: async Download() { try { const settings = { kit: true, tyres: true, serviced: false, }; const [kits, tyres] = await Promise.all([ this.c ...