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.

rj.learn
  • 645
  • 1
  • 6
  • 14
  • Check this example of mat dialog, make sure that all the required modules have been imported https://stackblitz.com/angular/gxyboyyobmo – Daniel C. Oct 25 '18 at 16:15
  • @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 not – rj.learn Oct 25 '18 at 16:20

5 Answers5

61

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();
    });
});
Yuri
  • 4,254
  • 1
  • 29
  • 46
dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
  • @dmcgrandie i agree that matdialog testing is tricky, thank you for your solution. it worked on my side. thank youuu – rj.learn Oct 26 '18 at 14:52
  • How do you trigger the `dialogRefSpyObj.afterClosed`? – PinguinoSod May 27 '20 at 08:21
  • @PinguinoSod - 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. – dmcgrandle May 28 '20 at 18:55
  • 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) – user3659739 Sep 23 '20 at 11:59
  • I would test the two modals independently (each with their own set of tests). – dmcgrandle Sep 24 '20 at 16:53
  • 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. – user3659739 Sep 25 '20 at 12:03
  • 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. – dmcgrandle Sep 25 '20 at 19:43
  • What happens when dialog is an independent component? – Van Wilder Jul 11 '22 at 12:33
15

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();
  });


}
George C.
  • 6,574
  • 12
  • 55
  • 80
  • 6
    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. – dmcgrandle Apr 12 '19 at 15:33
  • 3
    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. – ChumiestBucket Mar 03 '20 at 17:30
  • 1
    Please consider explaining how your code helps to solve OP's issue instead of plainly spoonfeeding code. – Edric Mar 29 '20 at 08:23
  • 1
    Thanks, it worked in my case, as mocking the entire dialog response, solved the issue and returned true/false as required. – Ashwin Nov 10 '20 at 03:44
7

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();
  });

});
Maarti
  • 3,600
  • 4
  • 17
  • 34
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();
   }));
 });
Nimantha
  • 165
  • 3
  • 11
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 })

Jared Beach
  • 2,635
  • 34
  • 38