Ak777
Ak777

Reputation: 396

Angular Unit testing on a component using Material Dialog for alerts is not getting initialized

After going through many topics in StackOverflow and other forums, i am giving up to try and willing to post my issue as a question.

I have a component that uses Material Dialog to show alerts like Confirmation popups or Information popups for my app. I created a component called AlertsComponent and using that in my parent components wherever i want to show the alerts. I have my own model to handle the information. All of it is working fine but the spec.ts (test case) is failing even on the create/initialize event.

My AlertsComponent.ts:

import { Component, OnInit, Optional, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { AlertInfo } from 'src/Model/common/alert-info.model';

@Component({
  selector: 'app-alerts',
  templateUrl: './alerts.component.html',
  styleUrls: ['./alerts.component.css']
})
export class AlertsComponent implements OnInit {

  constructor(
    private dialogRef: MatDialogRef<AlertsComponent>,
    @Optional() @Inject(MAT_DIALOG_DATA) public alertInfo?: AlertInfo
  ) {
    console.log('Alert Data: ' + JSON.stringify(alertInfo));
    if (alertInfo.ConfirmPopup) {
      alertInfo.Header = 'Confirm ?';
    } else { alertInfo.Header = 'Alert'; }
    this.dialogRef.disableClose = true;
  }

  ngOnInit() {
  }

  ConfirmResponse(response: boolean): void {
    this.dialogRef.close(response);
  }

  CloseAlert() {
    this.dialogRef.close();
  }

}

My HTML looks like:

<div>
  <h2 mat-dialog-title>{{alertInfo.Header}}</h2>
  <hr/>
  <mat-dialog-content>
      <strong>{{alertInfo.Body}}</strong>
      <br>
      <br>
      <!-- <strong>{{data}}</strong> -->
    </mat-dialog-content>
    <hr>
    <mat-dialog-actions>
      <div>
        <ng-container *ngIf="alertInfo.ConfirmPopup; else alertOnly">
            <button mat-button class="align-self-center" color="primary" class="button-space" (click)="ConfirmResponse(true);">YES</button>
            <button mat-button class="align-self-center" color="primary" class="button-space" (click)="ConfirmResponse(false);">NO</button>
        </ng-container>
        <ng-template #alertOnly>
            <button mat-button color="primary" class="button-space" (click)="CloseAlert();">OK</button>
        </ng-template>
      </div>
    </mat-dialog-actions>
</div>

And my spec.ts is:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { AlertsComponent } from './alerts.component';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material';
import { AlertInfo } from 'src/Model/common/alert-info.model';
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';

describe('AlertsComponent', () => {
  let component: AlertsComponent;
  let fixture: ComponentFixture<AlertsComponent>;
  let mockDialogRef: MatDialogRef<AlertsComponent>;
  let mockAlertInfoObj: AlertInfo;
  // const MY_MAT_MOCK_TOKEN = new InjectionToken<AlertInfo>('Mock Injection Token', {
  //   providedIn: 'root',
  //   factory: () => new AlertInfo()
  // });

  @Component({
    selector: 'app-alerts',
    template: '<div><mat-dialog-content></mat-dialog-content></div>'
  })

  class MockAlertsComponent { }

  mockDialogRef = TestBed.get(MatDialogRef);
  mockAlertInfoObj = new AlertInfo();
  mockAlertInfoObj.ConfirmPopup = false;
  mockAlertInfoObj.Body = 'test alert';

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AlertsComponent, MockAlertsComponent ],
      imports: [MatDialogModule],
      providers: [
        {provide: MatDialogRef, useValue: mockDialogRef},
        {provide: MAT_DIALOG_DATA, useValue: mockAlertInfoObj},
      ],
      schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
    })
    .compileComponents();
  }));

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

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

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

When i run "ng test", this component test case fails with error saying:

AlertsComponent encountered a declaration exception
Error: Cannot call Promise.then from within a sync test.
Error: Cannot call Promise.then from within a sync test.
    at SyncTestZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.SyncTestZoneSpec.onScheduleTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:366:1)
    at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:404:1)
    at Zone../node_modules/zone.js/dist/zone.js.Zone.scheduleTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:238:1)
    at Zone../node_modules/zone.js/dist/zone.js.Zone.scheduleMicroTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:258:1)
    at scheduleResolveOrReject (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:879:1)
    at ZoneAwarePromise.then (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:1012:1)
    at ApplicationInitStatus.push../node_modules/@angular/core/fesm5/core.js.ApplicationInitStatus.runInitializers (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm5/core.js:15618:1)
    at TestBedViewEngine.push../node_modules/@angular/core/fesm5/testing.js.TestBedViewEngine._initIfNeeded (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm5/testing.js:1702:59)
    at TestBedViewEngine.push../node_modules/@angular/core/fesm5/testing.js.TestBedViewEngine.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm5/testing.js:1766:1)
    at Function.push../node_modules/@angular/core/fesm5/testing.js.TestBedViewEngine.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm5/testing.js:1551:1)

I dont know or could figure out, where i am doing something wrong, or where? Can someone please help me?

Upvotes: 0

Views: 4089

Answers (2)

Erbsenkoenig
Erbsenkoenig

Reputation: 1659

So to start with, I would use the shallow testing approach to test that component and would use a test setup like this:

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

  let test: AlertInfo = {Header: 'HEADER', Body: 'BODY'};

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent, TestMatDialogActionsComponent, TestMatDialogContentComponent],
      providers: [
        {provide: MatDialogRef, useValue: {}},
        {provide: MAT_DIALOG_DATA, useValue: test}
      ],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));

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

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

In this setup the TestMatDialogActionsComponent and TestMatDialogContentComponent are needed to mock the material dialog stuff.

Those testing components could either be declared inside the spec file itself (but don't export them) or you could create a central test folder next to your src folder, where you put those components and export them, so you can reuse them in your tests. But make sure to only include this folder inside your tsconfig.spec.ts and not inside your tsconfig.app.ts just to make sure this component is not accidentally used inside your app.

@Component({
  selector: '[mat-dialog-actions]',
  template: '<ng-content></ng-content>'
})
export class TestMatDialogActionsComponent {

  constructor() { }
}

@Component({
  selector: '[mat-dialog-content]',
  template: '<ng-content></ng-content>'
})
export class TestMatDialogContentComponent {

  constructor() { }
}

Starting with that setup you can add everything you need, to test your use cases.

Upvotes: 1

Ak777
Ak777

Reputation: 396

@erbsenkoenig's stackblitz helped, and then i added few more to solve my need. below is what i did to mock and the MatDialog.

    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.
  public open(inputdata: any) {
    return {
      afterClosed: () => of({inputdata})
    };
  }
}

I referenced this from various other Stackoverflow answers to be honest. Used this mock class in the providers. Then had my tests instantiated it as

// arrange
const mockAddEditDialogObj = MatDialogMock.prototype;
let dialogRef = jasmine.createSpyObj(mockAddEditDialogObj.open.name, ['afterClosed']);
dialogRef.afterClosed.and.returnValue(of(true));

// act
component.AddNew();
dialogRef = mockAddEditDialogObj.open(EditProjectComponent.prototype);
const result = dialogRef.afterClosed();

// assert
expect(dialogRef).toBeTruthy();

You can expand the tests and mock class return objects as needed for your tests.

Upvotes: 0

Related Questions