Модульное тестирование Angular на компоненте, использующем диалоговое окно Material для оповещений, не инициализируется

#angular #jasmine #angular-material

#angular #jasmine #angular-material

Вопрос:

После просмотра многих тем в StackOverflow и других форумах я отказываюсь от попыток и готов опубликовать свою проблему в виде вопроса.

У меня есть компонент, который использует Material Dialog для отображения оповещений, таких как всплывающие окна подтверждения или информационные окна для моего приложения. Я создал компонент под названием AlertsComponent и использую его в своих родительских компонентах везде, где хочу показывать оповещения. У меня есть своя модель для обработки информации. Все это работает нормально, но spec.ts (тестовый пример) завершается сбоем даже при событии create / initialize.

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

}  

Мой HTML выглядит так:

 <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>  

И мой spec.ts является:

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

Когда я запускаю «ng test», этот тестовый пример компонента завершается ошибкой с сообщением:

 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)
  

Я не знаю или не мог понять, где я делаю что-то не так, или где? Кто-нибудь, пожалуйста, может мне помочь?

Комментарии:

1. Мне интересно, какова цель предоставления макета и реального компонента для одного и того же селектора? И в дополнение к этому я не совсем уверен, возможно ли получить доступ к TestBed.get() внутри издевательского компонента. Это может быть проблемой. Не могли бы вы, пожалуйста, предоставить stackblitz или что-то подобное. Чем проще выявить проблему.

2. Спасибо, я создал мини-версию с точной имитацией alertsComponent в stackblitz.com/edit/angular-pvgpjg . Теперь, вкратце — все, что мне нужно, это настроить тесты для этого. Стандартный тестовый набор «it should create» по умолчанию не выполняется, и я буквально добавляю все (по моему предположению) варианты соответствия, чтобы исправить это, из поиска в Google. Я могу предположить, что это должно быть просто в противном случае, чего я сам не мог себе представить.

3. Хорошо, завтра я создам stackblitz с настройками тестирования

4. Удалось ли вам настроить тестирование? приветствуется любая помощь.

5. Начальная настройка теста может быть примерно такой: stackblitz.com/edit/directive-testing-17bcms — Моим app.component будет ваш alert.component

Ответ №1:

Итак, для начала я бы использовал подход к неглубокому тестированию для тестирования этого компонента и использовал бы тестовую настройку, подобную этой:

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

В этой настройке TestMatDialogActionsComponent и TestMatDialogContentComponent необходимы для макетирования содержимого диалогового окна material.

Эти компоненты тестирования могут быть либо объявлены внутри самого файла спецификации (но не экспортировать их), либо вы могли бы создать центральную папку тестирования рядом с вашей папкой src, куда вы помещаете эти компоненты и экспортируете их, чтобы вы могли повторно использовать их в своих тестах. Но убедитесь, что эта папка включена только внутри вашего, tsconfig.spec.ts а не внутри вашего tsconfig.app.ts , чтобы убедиться, что этот компонент случайно не используется внутри вашего приложения.

 @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() { }
}
  

Начиная с этой настройки, вы можете добавить все, что вам нужно, для тестирования ваших вариантов использования.

Комментарии:

1. Я рвал на себе волосы, пытаясь устранить ошибку: «директива matdialog не имеет селектора, пожалуйста, добавьте его» во время тестирования компонента, который является дочерним для маршрутизатора-выхода, содержащегося в matdialog. Ваше единственное решение, которое сработало, большое вам спасибо!

Ответ №2:

stackblitz от @erbsenkoenig помог, а затем я добавил еще несколько, чтобы решить свои потребности. ниже показано, что я сделал для mock и 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})
    };
  }
}
  

Честно говоря, я ссылался на это из различных других ответов Stackoverflow. Использовал этот макет класса в поставщиках. Затем мои тесты создали его как

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

Вы можете расширить тесты и макет возвращаемых объектов класса по мере необходимости для ваших тестов.