MatDialog Service Unit Test Angular 6 Error
Asked Answered
M

6

43

I'm having modal service to open, confirm and close dialog and i am making its unit test file but i got and error on Angular and this is the code.

modal.service.ts

@Injectable()
export class ModalService {

  constructor(private dialog: MatDialog) { }

  public open<modalType>(modalComponent: ComponentType<modalType>): Observable<any> {
    let dialogRef: MatDialogRef<any>;

    dialogRef = this.dialog.open(modalComponent, {
      maxWidth: '100vw'
    });
    console.log(dialogRef)
    dialogRef.componentInstance.body = body;

    return dialogRef.afterClosed().pipe(map(result => console.log('test'); );
  }

}

modal.service.spec.ts

export class TestComponent  {}


describe('ModalService', () => {
  let modalService: ModalService;

  const mockDialogRef = {
    open: jasmine.createSpy('open')
  };

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ MatDialogModule ],
      providers: [
        ModalService,
        MatDialogRef,
        { provide: MatDialog, useClass: MatDialogStub }
      ]
    }).compileComponents();

    modalService = TestBed.get(ModalService);
  }));


  it('open modal', () => {
    modalService.open(DummyComponent, '300px');
    expect(modalService.open).toHaveBeenCalled();

  });

});

So with that code the error is

TypeError: Cannot read property 'componentInstance' of undefined

Can you help me how to make this successful? Help is much appreciated.

Millar answered 25/10, 2018 at 15:58 Comment(2)
Check this example of mat dialog, make sure that all the required modules have been imported stackblitz.com/angular/gxyboyyobmoSoftspoken
@DanielC. hey thank you for your suggestion, but i was looking for unit test answer. the service is working well being called in the component but in unit test is notMillar
L
62

Testing mat-dialogs can be tricky. I tend to use a spy object for the return from a dialog open (dialogRefSpyObj below) so I can more easily track and control tests. In your case it might look something like the following:

describe('ModalService', () => {
    let modalService: ModalService;
    let dialogSpy: jasmine.Spy;
    let dialogRefSpyObj = jasmine.createSpyObj({ afterClosed : of({}), close: null });
    dialogRefSpyObj.componentInstance = { body: '' }; // attach componentInstance to the spy object...

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [MatDialogModule],
            providers: [ModalService]
        });
        modalService = TestBed.get(ModalService);
    });

    beforeEach(() => {
        dialogSpy = spyOn(TestBed.get(MatDialog), 'open').and.returnValue(dialogRefSpyObj);
    });

    it('open modal ', () => {
        modalService.open(TestComponent, '300px');
        expect(dialogSpy).toHaveBeenCalled();

        // You can also do things with this like:
        expect(dialogSpy).toHaveBeenCalledWith(TestComponent, { maxWidth: '100vw' });

        // and ...
        expect(dialogRefSpyObj.afterClosed).toHaveBeenCalled();
    });
});
Lyndsaylyndsey answered 26/10, 2018 at 0:0 Comment(8)
@dmcgrandie i agree that matdialog testing is tricky, thank you for your solution. it worked on my side. thank youuuMillar
How do you trigger the dialogRefSpyObj.afterClosed?Zebadiah
@Zebadiah - How you trigger it depends on your code. Feel free to ask another question with your details. Be sure to include code examples showing what you have tried so far.Lyndsaylyndsey
How would you do for testing a modal which is open by another modal ? ( app > click button > 1st basic confirmation modal > click yes > open component in a new modal)Unclog
I would test the two modals independently (each with their own set of tests).Lyndsaylyndsey
Yes but the problem is that when i put a spy on MatDialog it always return me the first dialog opened so i don't know how to spy the second dialog which is open by the first dialog.Unclog
If you are unit testing, then don't test the second WITH the first, test them both in isolation. Then the "first" dialog opened will be the only one you are testing. If you are doing integration testing or end-to-end testing you'll want to use a different tool such as cypress.io. This question is referencing unit testing. If you need more help, I would suggest asking another StackOverflow question rather than continuing a chat dialog.Lyndsaylyndsey
What happens when dialog is an independent component?Lecture
C
16

I have a better solution that still works on 2019

header.component.ts

import { BeforeLogOutComponent } from '@app-global/components/before-log-out/before-log-out.component';


  /**
   * The method triggers before the logout.
   * Opens the dialog and warns the user before log Out.
   */
  public beforeLogOut(): void {
    this._dialog.open(BeforeLogOutComponent, { width: '400px', disableClose: false, panelClass: 'dialog_before_log_out' })
    .afterClosed()
    .subscribe((res) => {
      if (res && res.action === true) { this.loggedOut(); }
    }, err => {
      console.error(err);
    });
  }

header.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material';
import { Observable, of } from 'rxjs';



<<-- Create a MatDialog mock class -->>
export class MatDialogMock {
  // When the component calls this.dialog.open(...) we'll return an object
  // with an afterClosed method that allows to subscribe to the dialog result observable.
  open() {
    return {
      afterClosed: () => of({action: true})
    };
  }
}



describe('HeaderComponent', () => {

  let component: HeaderComponent;
  let fixture: ComponentFixture<HeaderComponent>;

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      imports: [
        MaterialModule, RouterTestingModule, HttpModule, BrowserAnimationsModule,
        HttpClientModule, FlexLayoutModule,
      ],
      declarations: [
        HeaderComponent,
      ],
      providers: [
        { provide: MatDialog, useClass: MatDialogMock } <<-- look this
      ]
    })
    .compileComponents();
  }));



  beforeEach(async() => {
    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance;


    component.ngOnInit();
    component.ngAfterViewInit();
    await fixture.whenStable();
    fixture.detectChanges();
  });


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


  // I test the dialog here.
  fit('should open the dialog', () => {
    component.beforeLogOut();
  });


}
Cajun answered 14/2, 2019 at 15:35 Comment(4)
Actually the spy object solution in my other answer above works just fine in Angular 7 in 2019. :) I prefer that over the mock class approach you give, though they will both solve the problem, it is just a matter of preference which one to chose.Lyndsaylyndsey
hmm, not sure how this would test afterClosed as it's never actually called, because nothing tells the Dialog to close in the spec file.Microbalance
Please consider explaining how your code helps to solve OP's issue instead of plainly spoonfeeding code.Sequestrate
Thanks, it worked in my case, as mocking the entire dialog response, solved the issue and returned true/false as required.Petersburg
P
8

I do not have the exact answer for your case but I also did some tests on MatDialog. I can show you what I did. Maybe look at the inject() part:

(I deleted some things for clarity and confidentiality)

describe('MyDialogComponent', () => {
  let dialog: MatDialog;
  let overlayContainer: OverlayContainer;
  let component: MyDialogComponent;
  let fixture: ComponentFixture<MyDialogComponent>;
  const mockDialogRef = {
    close: jasmine.createSpy('close')
  };

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        BrowserAnimationsModule,
        ReactiveFormsModule,
        AngularMaterialModule,
      ],
      providers: [
        { provide: MatDialogRef, useValue: mockDialogRef },
        {
          provide: MAT_DIALOG_DATA,
          useValue: {
            title: 'myTitle',
          }
        }
      ],
      declarations: [MyDialogComponent],
    });

    TestBed.overrideModule(BrowserDynamicTestingModule, {
      set: {
        entryComponents: [MyDialogComponent]
      }
    });

    TestBed.compileComponents();
  }));

  beforeEach(inject([MatDialog, OverlayContainer],
    (d: MatDialog, oc: OverlayContainer) => {
      dialog = d;
      overlayContainer = oc;
    })
  );

  afterEach(() => {
    overlayContainer.ngOnDestroy();
  });

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

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


  it('onCancel should close the dialog', () => {
    component.onCancel();
    expect(mockDialogRef.close).toHaveBeenCalled();
  });

});
Proser answered 25/10, 2018 at 23:4 Comment(0)
C
2

Add this part to the providers section

{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} },

Check below

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
       imports: [
          MatDialogModule,
       ],
       declarations: [MyDialogComponent],
       providers: [
          { provide: MAT_DIALOG_DATA, useValue: {} },
          { provide: MatDialogRef, useValue: {} },
       ],
    }).compileComponents();
   }));
 });
Calomel answered 24/8, 2021 at 7:26 Comment(0)
D
0

This answer doesn't directly answer the question, but is for people like me who ended up here because you couldn't query for the dialog after it opened.

With spectator you just have to include { root: true } as an option to your query.

The dialog doesn't get found because it's not a child of the component you're testing, but using { root: true } will search the whole page.

Ex: spectator.query(byTestId('myTestId'), { root: true })

Deangelis answered 15/10, 2021 at 1:7 Comment(0)
D
0

it('should open a dialogbox', () => {

    //arrange
    const dialog = TestBed.inject(MatDialog); 

    //act 
    spyOn(dialog,'open').and.callThrough();
    component.openDialog();
    
    //assert
    expect(dialog.open).toHaveBeenCalled();
})
Demolition answered 31/10, 2023 at 8:31 Comment(2)
Welcome to Stack Overflow! Thank you for your answer. Please provide more details about your solution. Code snippets, high quality descriptions, or any relevant information would be great. Clear and concise answers are more helpful and easier to understand for everyone. Edit your answer with specifics to raise the quality of your answer. For more information: How To: Write good answers. Happy coding!Brazenfaced
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Tamarin

© 2022 - 2025 — McMap. All rights reserved.