Challenges with invoking functions within ngFor loop during unit testing in Angular 12

I am currently learning about unit testing and I am facing an issue with calling a function inside an *ngFor loop in an Angular 12 application. I have an observable that contains an array of objects and it is working correctly, iterating through the data properly.

Below is the HTML code:

<div class="table-responsive">
    <table class="table table-striped">
        <thead>
          <tr>
            <th>#</th>
            <th>Name</th>
            <th>Category</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let item of games$ | async; let i = index">
            <td>{{ i+1 }}</td>
            <td>{{ item.name }}</td>
            <td class="badge-cell">
               {{ item.categoryId }}
            </td>
            <td class="center action-cell">
              <button type="button"
                class="delete-game btn"
                (click)="deleteGame(item.id)">Delete</button>
            </td>
          </tr>
        </tbody>
    </table>
</div>
<!-- /.table-responsive-->

The problem occurs when trying to test a function call inside the loop. Accessing the nativeElement property of a fixture shows that the dynamic part of the table is null, which means the deleteGame function is not available.

Here is what console.log(fixture.nativeElement) looks like:

<div _ngcontent-a-c74="" class="table-responsive">
    <table _ngcontent-a-c74="" id="dataTables-example" class="table table-striped">
        <thead _ngcontent-a-c74="">
            <tr _ngcontent-a-c74="">
                <th _ngcontent-a-c74="">#</th>
                <th _ngcontent-a-c74="">Name</th>
                <th _ngcontent-a-c74="">Category</th>
                <th _ngcontent-a-c74="">Action</th>
            </tr>
        </thead>
        <tbody>
         //----------NO ROWS IN THE LOG------------//
        <!--bindings={
          "ng-reflect-ng-for-of": null
        }-->
        </tbody>
    </table>
</div>

Static data is being used to simulate the original array (see GAME_DATA). Here are the tests:

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

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, CommonModule],
        declarations: [ListGamesComponent],
        providers: [
          RequestService,
        ],
      })
        .compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(ListGamesComponent);
          component = fixture.componentInstance as any;
          component.games$ = from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >;
        });
    })
  );

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

  it('should check if deleteGame was called', fakeAsync(() => {
    spyOn(component, 'deleteGame');
    
    component.games$.subscribe((data: any) => {
     // ---- logged an observable, a proper list of games is returned ----
      console.log('data: ', data);
    });
    
    fixture.detectChanges();

    let button = fixture.debugElement.query(By.css('.delete-game'));
    
    button.triggerEventHandler('click', null);
    fixture.detectChanges();
    tick();

    expect(component.deleteGame).toHaveBeenCalled();
  }));
});

In addition, I logged an observable inside the test and it retrieved the data from GAME_DATA static file, so I am confused as to why the table is not being generated.

UPDATE

This is how games$ is implemented in the component:

export class ListGamesComponent implements OnInit {

  ngOnInit(): void {
    let pager: { index: number; size: number } = {
      index: this.pageIndex,
      size: this.pageSize,
    };
    this.games$ = this.gameService.getAllGames(pager).pipe(
      tap((data) => {
        this.totalRecords = data.totalItems;
        this.gamesList = data.games;
        
        this.setPage(1, false); // generate pagination
      }),
      pluck('games')
    );
  }
}

Any assistance would be highly appreciated. Thank you!

Answer №1

Let me walk you through what's happening before we resolve the issue, so you have a clearer picture.

The initial call of fixture.detectChanges() coincides with the invocation of ngOnInit (crucial to remember).

// modifying the definition of games$
component.games$ = from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >;
// subscribing and observing the data
component.games$.subscribe((data: any) => {
     // ---- receiving an observable, a valid list of games is returned ----
      console.log('data: ', data);
    });
// triggering fixture.detectChanges(); which in turn triggers ngOnInit
fixture.detectChanges();

The issue arises because ngOnInit reassigns this.games$ to a service call, leading to the loss of the previously assigned definition. This is the cause of the problem you are encountering. I hope this clarifies things.

To rectify this:

I prefer utilizing createSpyObj to simulate external service dependencies, follow the instructions marked with !! in the comments.

describe('ListGamesComponent', () => {
  let component: ListGamesComponent;
  let fixture: ComponentFixture<ListGamesComponent>;
  // !! create a mock game service
  let mockGameService: jasmine.SpyObj<GameService>;

  beforeEach(
    waitForAsync(() => {
      // !! assign mockGameService to a spy object with a public method of
      // getAllGames
      mockGameService = jasmine.createSpyObj<GameService>('GameService', ['getAllGames']);
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, CommonModule],
        declarations: [ListGamesComponent],
        providers: [
          RequestService,
         // !! provide a fake GameService instead of the real GameService
         { provide: GameService, useValue: mockGameService },
        ],
      })
        .compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(ListGamesComponent);
          component = fixture.componentInstance as any;          
        });
    })
  );

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

  it('should check if deleteGame was called', fakeAsync(() => {
    spyOn(component, 'deleteGame');
    
    // !! set the mock to return a value before ngOnInit is triggered
    mockGameService.getAllGames.and.returnValue(from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >);
   
    fixture.detectChanges();

    let button = fixture.debugElement.query(By.css('.delete-game'));
    
    button.triggerEventHandler('click', null);
    fixture.detectChanges();
    tick();

    expect(component.deleteGame).toHaveBeenCalled();
  }));
});

Refer to this guide for how to mock external dependencies effectively.

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

Exploring the functionality of the WHERE function in Firebase with Angular

Current Objective: Our main focus here is to allow users to post within their designated Organization Group. These posts should remain exclusively visible within the specific Organization Group they are posted in. To achieve this, I have attempted to impl ...

Sending a JSON payload from Angular to C# can result in a value of 0 or null being received

As a beginner working on my Angular&C# project, I encountered an issue. Despite sending a JSON from Angular to C# (POST) with values, the C# side is not receiving anything (value 0 for int and null for string). I can't figure out what I'm doi ...

"Unlocking the potential: Exploring the power of keyof with

Currently, I am working with React Redux toolkit and keyof to specify that one element of my action payload should be the key of the type that my state is composed of. This allows me to update the properties of the state using a redux action. However, I am ...

Issue with displaying error message and disabling button as Keyup event fails to trigger

Is there a way to assess the user's input in real-time on an on-screen form to ensure that the pageName they enter is not already in the navbarMenuOptions array? If it is, I want to switch the visibility of displayName and displaySaveButton. However, ...

Tips for efficiently adding a like feature to a MEAN web application

Currently, I am in the process of developing a web application that allows users to express their preferences by liking certain choices displayed on the page. I am trying to optimize the efficiency of the like/unlike system. My main question is whether ev ...

When invoking a JavaScript method, the context variable 'this' is lost

I have developed a basic pointer to a method like this: export class SmbwaService { getExistingArsByLab(labId: number): Observable<SmwbaAr[]> { this.otherMethod(); } otherMethod(): void { } } let method: (x: number) => ...

Choose everything except for the information determined by the search

Currently facing an issue with the select all functionality. I found a code snippet on this link but it's not exactly what I need. I want to modify the select all feature so that it is based on the search value. For instance, if I have a set of data ...

Tips on preserving type safety after compiling TypeScript to JavaScript

TS code : function myFunction(value:number) { console.log(value); } JS code, post-compilation: function myFunction(value) { console.log(value); } Are there methods to uphold type safety even after the conversion from TypeScript to JavaScript? ...

VSC is throwing a type error, but is it still possible to compile the code?

It seems like after downloading some extensions, I started encountering this issue which was not present before. My files are now displaying errors even though everything should be fine. https://i.sstatic.net/cr7Ef.png The error seems to be related to ca ...

Error! Unable to Inject ComponentFactoryResolver

Recently, I attempted to utilize ComponentFactoryResolver in order to generate dynamic Angular components. Below is the code snippet where I am injecting ComponentFactoryResolver. import { Component, ComponentFactoryResolver, OnInit, ViewChild } from "@an ...

The chart is failing to refresh with the latest response information (using Chart.js version 3.2.1 and ng2-charts version 3.0.0-beta.9)

I recently upgraded my project using Chart.js version 3.2.1 and ng2-charts version 3.0.0-beta.9. Initially, everything seemed to be working fine with mock data - the charts were displaying as expected. However, when I switched to testing with real backend ...

Ways to RESOLVE implicit any for accessing array property

Check out the code snippet below: function getUsername(): string { return 'john_doe'; } function sampleFunction(): void { const data = {}; const username: string = getUsername(); const age: any = 30; data[username] = age; // ...

What's the best way for me to figure out whether type T is implementing an interface?

Is it possible to set a default value for the identifier property in TypeScript based on whether the type extends from Entity or not? Here's an example of what I'm trying to achieve: export interface Entity { id: number; // ... } @Compon ...

Understanding 'this' in ChartJS within an Angular application

Here is my event handler for chartJS in Angular that I created: legend: { onClick: this.toggleLegendClickHandler After changing the text of the y scale title, I need to update the chart. I am looking to accomplish this by calling this._chart.cha ...

Explicit declaration of default parameters

Check out the helpful solution In regard to the following code snippet, type C = { a: string, b: number } function f({ a, b } = {a:"", b:0}): void { // ... } Can you explain the syntax for explicitly typing the default parameter? ...

Utilizing a variety of Reactive FormGroups to manage a shared data source

My data source is structured as follows: data = { Bob: { hobbies: ['cycling', 'swimming'], pets: ['cat', 'dog'] }, Alice: { hobbies: ['cycling, 'chess'] ...

Tips on how to incorporate a .js file into my .tsx file

I ran into an issue with webpack displaying the following message: ERROR in ./app/app.tsx (4,25): error TS2307: Cannot find module './sample-data'. The imports in my code are as follows: import * as React from 'react'; import * ...

Launching a new tab with a specific URL using React

I'm attempting to create a function that opens a new tab with the URL stored in item.url. The issue is, the item.url property is provided by the client, not by me. Therefore, I can't guarantee whether it begins with https:// or http://. For insta ...

The functionality of the Ionic 4 app differs from that of an Electron app

I've encountered an issue with my Ionic 4 capacitor app. While it functions properly on Android studio, I'm having trouble getting it to work on Electron. Any ideas on how to resolve this? Here are the steps I took to convert it to Electron: np ...

Error in Redirecting to Localhost

Recently, I developed an Angular App that interacts with the MS Graph API using MSAL. Initially, everything worked smoothly when running "ng serve" in Angular CLI. However, I encountered a problem when I packaged this Angular App with electron to deploy i ...