Polkov
Polkov

Reputation: 43

TypeError: Cannot read properties of undefined (reading 'push') in Angular Material Dialog Testing

The previous answers to this question do not resolve my issue.

I'm encountering a TypeError while testing a method in my Angular component that opens a dialog to delete a user. The error message states:

TypeError: Cannot read properties of undefined (reading 'push') at MatDialog.open (node_modules/@angular/material/fesm2022/dialog.mjs:598:26) at MatDialog.open (node_modules/@angular/material/fesm2022/dialog.mjs:598:26) at UserComponent.deleteUser (src/app/components/user/user.component.ts:108:35) at UserContext.apply (src/app/components/user/user.component.spec.ts:147:15) at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28) at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2081:39) at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34) at ZoneImpl.run (node_modules/zone.js/fesm2015/zone.js:111:43) at runInTestZone (node_modules/zone.js/fesm2015/zone-testing.js:216:38) at UserContext. (node_modules/zone.js/fesm2015/zone-testing.js:234:32) at at UserContext.apply (src/app/components/user/user.component.spec.ts:147:15) at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28) at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2081:39) at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34) at ZoneImpl.run (node_modules/zone.js/fesm2015/zone.js:111:43) at runInTestZone (node_modules/zone.js/fesm2015/zone-testing.js:216:38)

Here's the relevant portion of my UserComponent:

deleteUser(userId: number, userNickname: string): void {
  const dialogRef = this.dialog.open(DeleteDialogComponent, {
    height: '400px',
    width: '550px',
    data: { userId, userNickname },
  });

  dialogRef.afterClosed().subscribe(result => {
    if (result) {
      const subscription = this.userService.deleteUser(userId).subscribe({
        next: () => {
          console.log("Ok!")
        },
        error: error => {
        }
      });
    }
  });
}

And here is my test setup:

describe('UserComponent', () => {
  let component: UserComponent;
  let fixture: ComponentFixture<UserComponent>;
  let userServiceMock: jasmine.SpyObj<UserService>;
  let routerMock: jasmine.SpyObj<Router>;
  let dialogSpy: jasmine.SpyObj<MatDialog>;

  beforeEach(async () => {
    userServiceMock = jasmine.createSpyObj<UserService>('UserService', {
      deleteUser: of(1)
    });
    routerMock = jasmine.createSpyObj<Router>('Router', ['navigate']);
    dialogSpy = jasmine.createSpyObj('MatDialog', ['open']);

    await TestBed.configureTestingModule({
      imports: [UserComponent, MatDialogModule, BrowserAnimationsModule],
      providers: [
        { provide: UserService, useValue: userServiceMock },
        { provide: Router, useValue: routerMock },
        { provide: MatDialog, useValue: dialogSpy }
      ]
    }).compileComponents();

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

  it('should open modal and delete user', () => {
    const userId = 1;
    const userNickname = 'JohnSmith';

    component.deleteUser(userId, userNickname);

    const dialogRef = dialogSpy.open.calls.mostRecent().returnValue;
    dialogRef.afterClosed = jasmine.createSpy().and.returnValue(of(true));

    component.deleteUser(userId, userNickname);
    
    expect(userServiceMock.deleteUser).toHaveBeenCalledWith(userId);
  });
});

The line this.openDialogs.push(dialogRef); in the MatDialog implementation seems to be the source of the issue.

I suspect that the dialogRef is not being created correctly during the test, but I'm unsure how to resolve this.

How can I ensure that dialogRef is correctly mocked so that the openDialogs array is not undefined? Are there any adjustments I need to make to my test setup or the deleteUser method to avoid this error? Any insights or suggestions would be greatly appreciated!

Upvotes: 3

Views: 448

Answers (1)

Naren Murali
Naren Murali

Reputation: 57986

You should spyOn open method and return an object that contains the property afterClosed, this will be a function which returns on observable.

We can use fakeAsync and flush to wait for the async observable to complete, then verify the result.

it('should open modal and delete user', fakeAsync(() => {
  const userId = 1;
  const userNickname = 'JohnSmith';
  dialogSpy.open.and.returnValue({
    afterClosed: () => of(true),
  } as any);
  component.deleteUser(userId, userNickname);
  flush();
  expect(userServiceMock.deleteUser).toHaveBeenCalledWith(userId);
}));

Full Code:

import {
  TestBed,
  ComponentFixture,
  waitForAsync,
  fakeAsync,
  flush,
} from '@angular/core/testing';
import { AppComponent, UserService } from './app.component';
import { AppModule } from './app.module';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { of } from 'rxjs';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let userServiceMock: jasmine.SpyObj<UserService>;
  let routerMock: jasmine.SpyObj<Router>;
  let dialogSpy: jasmine.SpyObj<MatDialog>;

  beforeEach(async () => {
    userServiceMock = jasmine.createSpyObj<UserService>('UserService', {
      deleteUser: of(1),
    });
    routerMock = jasmine.createSpyObj<Router>('Router', ['navigate']);
    dialogSpy = jasmine.createSpyObj('MatDialog', ['open']);

    await TestBed.configureTestingModule({
      imports: [AppModule],
      providers: [
        { provide: UserService, useValue: userServiceMock },
        { provide: Router, useValue: routerMock },
        { provide: MatDialog, useValue: dialogSpy },
      ],
    }).compileComponents();

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

  it('should open modal and delete user', fakeAsync(() => {
    const userId = 1;
    const userNickname = 'JohnSmith';
    dialogSpy.open.and.returnValue({
      afterClosed: () => of(true),
    } as any);
    component.deleteUser(userId, userNickname);
    flush();
    expect(userServiceMock.deleteUser).toHaveBeenCalledWith(userId);
  }));
});

Stackblitz Demo

Upvotes: 0

Related Questions