0
votes

I have in my angular7 app a 'Settings' module, which uses the Ngrx library. My objective is to test unitarily the Component template that launches it. When using a MatSlideToggle function of Angular Material, the on / off control can be alternated by clicking on the modal component. However, the result of the Karma ~3.1.1 and Jasmine ^2.99.0 test returns the event as undefined. I get the error "can not read property 'triggereventhandler' of undefined".

Note: I sincerely thank Mr. Tomas Trajan (@tomastrajan) and the rest of the contributors to the open project "Angular NgRx Material Starter".

I thank you in advance for your attention in my first question.

This is my 'ajustes-cont.component.spec.ts' file:

import { By } from '@angular/platform-browser';
import { ComponentFixture, async, TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { MatSlideToggle } from '@angular/material';

import { MockStore, TestingModule } from '../../../testing/utils';

import { AjustesContComponent } from "./ajustes-cont.component";
import {
  ActionAjustesCambiarNavbarPegado,
  ActionAjustesCambiarTema,
  ActionAjustesCambiarAutoNocheModo,
  ActionAjustesCambiarAnimacionesPagina,
  ActionAjustesCambiarAnimacionesElementos
} from '../ajustes.actions';

describe('AjustesContComponent', () => {
  let component: AjustesContComponent;
  let fixture: ComponentFixture<AjustesContComponent>;
  let store: MockStore<any>;
  let dispatchSpy;

  const getThemeSelectArrow = () =>
    fixture.debugElement.queryAll(By.css('.mat-select-trigger'))[1];
  const getSelectOptions = () =>
    fixture.debugElement.queryAll(By.css('mat-option'));

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [TestingModule],
      declarations: [AjustesContComponent]
    }).compileComponents();

    store = TestBed.get(Store);
    store.setState({
      settings: {
        tema: 'DEFECTO-TEMA', // scss theme
        autoNocheModo: true, // auto night mode
        navbarPegado: true, // sticky Header
        paginaAnimaciones: true, // page animations
        paginaAnimacionesDisabled: false, //// page animations disabled
        elementosAnimaciones: true, // elements animations
        idioma: 'esp' // default language
      }
    });
    fixture = TestBed.createComponent(AjustesContComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('debe disparar el cambio del Navbar Pegado en el conmutador del navbarPegado', () => { // 'should dispatch change sticky header on sticky header toggle'
    dispatchSpy = spyOn(store, 'dispatch');
    const componentDebug = fixture.debugElement;
    const slider = componentDebug.queryAll(By.directive(MatSlideToggle))[0];

    slider.triggerEventHandler('change', { checked: false });
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expect(dispatchSpy).toHaveBeenCalledWith(
      new ActionAjustesCambiarNavbarPegado({ navbarPegado: false })
    );
  });

  it('debe disparar el cambio de tema en la selección de tema', () => { // 'should dispatch change theme action on theme selection'
    dispatchSpy = spyOn(store, 'dispatch');
    getThemeSelectArrow().triggerEventHandler('click', {});
    fixture.detectChanges();

    getSelectOptions()[1].triggerEventHandler('click', {});
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expect(dispatchSpy).toHaveBeenCalledWith(
      new ActionAjustesCambiarTema({ tema: 'AZUL-TEMA' })
    );
  });

  it('debe disparar el cambio del Auto Noche Modo en el conmutador del autoNocheModo', () => { // 'should dispatch change auto night mode on night mode toggle'
    dispatchSpy = spyOn(store, 'dispatch');
    const componentDebug = fixture.debugElement;
    const slider = componentDebug.queryAll(By.directive(MatSlideToggle))[1];

    slider.triggerEventHandler('change', { checked: false });
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expect(dispatchSpy).toHaveBeenCalledWith(
      new ActionAjustesCambiarAutoNocheModo({ autoNocheModo: false })
    );
  });

  it('debe disparar el cambio de las paginaAnimaciones', () => { // 'should dispatch change animations page'
    dispatchSpy = spyOn(store, 'dispatch');
    const componentDebug = fixture.debugElement;
    const slider = componentDebug.queryAll(By.directive(MatSlideToggle))[2];

    slider.triggerEventHandler('change', { checked: false });
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expect(dispatchSpy).toHaveBeenCalledWith(
      new ActionAjustesCambiarAnimacionesPagina({ paginaAnimaciones: false })
    );
  });

  it('debe disparar el cambio de los elementosAnimaciones', () => { // 'should dispatch change animations elements'
    dispatchSpy = spyOn(store, 'dispatch');
    const componentDebug = fixture.debugElement;
    const slider = componentDebug.queryAll(By.directive(MatSlideToggle))[3];

    slider.triggerEventHandler('change', { checked: false });
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expect(dispatchSpy).toHaveBeenCalledWith(
      new ActionAjustesCambiarAnimacionesElementos({ elementosAnimaciones: false })
    );
  });

  it('debe deshabilitar las paginaAnimaciones cuando se desactiva la configuración', () => { // 'should disable change animations page when disabled is set in state'
    store.setState({
      settings: {
        tema: 'DEFECTO-TEMA',
        autoNocheModo: true,
        paginaAnimaciones: true,
        paginaAnimacionesDisabled: true, // change animations disabled
        elementosAnimaciones: true,
        idioma: 'esp'
      }
    });
    fixture.detectChanges();

    dispatchSpy = spyOn(store, 'dispatch');
    const componentDebug = fixture.debugElement;
    const slider = componentDebug.queryAll(By.directive(MatSlideToggle))[2];

    slider.triggerEventHandler('change', { checked: false });
    fixture.detectChanges();

    expect(dispatchSpy).toHaveBeenCalledTimes(0);
  });

});

This is my 'settings-container.component.ts' file:

import {
      Component,
      OnInit,
      ChangeDetectionStrategy,
      ChangeDetectorRef
     } from '@angular/core';
    import { Store, select } from '@ngrx/store';
    import { Observable  } from 'rxjs';

    import { ANIMACIONES_RUTA_ELEMENTOS } from '../../nucleo';

    import {
      ActionAjustesCambiarIdioma,
      ActionAjustesCambiarTema,
      ActionAjustesCambiarAutoNocheModo,
      ActionAjustesCambiarNavbarPegado,
      ActionAjustesCambiarAnimacionesPagina,
      ActionAjustesCambiarAnimacionesElementos
    } from '../ajustes.actions';
    import { AjustesState, State } from '../ajustes.model';
    import { selectAjustes } from '../ajustes.selectors';

    @Component({
      selector: 'bab-ajustes-cont',
      templateUrl: './ajustes-cont.component.html',
      styleUrls: ['./ajustes-cont.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class AjustesContComponent implements OnInit {
      routeAnimationsElements = ANIMACIONES_RUTA_ELEMENTOS;
      ajustes$: Observable<AjustesState>;

      temas = [
        { value: 'DEFECTO-TEMA', label: 'verde' },
        { value: 'AZUL-TEMA', label: 'azul' },
        { value: 'PURPURA-TEMA', label: 'purpura' },
        { value: 'NEGRO-TEMA', label: 'negro' }
      ];

      idiomas = [
        { value: 'esp', label: 'esp' },
        { value: 'val-cat', label: 'val-cat' },
        { value: 'ing', label: 'ing' },
        { value: 'ale', label: 'ale' },
        { value: 'fra', label: 'fra' }
      ];

      constructor(private store: Store<State>) { }

      ngOnInit() {
        this.ajustes$ = this.store.pipe(select(selectAjustes));
      }

      idiomaSelect({ value: idioma }) {
        this.store.dispatch(new ActionAjustesCambiarIdioma({ idioma }));
      }

      temaSelect({ value: tema }) {
        this.store.dispatch(new ActionAjustesCambiarTema({ tema }));
      }

      autoNocheModoToggle({ checked: autoNocheModo }) {
        this.store.dispatch(
          new ActionAjustesCambiarAutoNocheModo({ autoNocheModo })
        );
      }

      navbarPegadoToggle({ checked: navbarPegado }) {
        this.store.dispatch(new ActionAjustesCambiarNavbarPegado({ navbarPegado }));
      }

      paginaAnimacionesToggle({ checked: paginaAnimaciones }) {
        this.store.dispatch(
          new ActionAjustesCambiarAnimacionesPagina({ paginaAnimaciones })
        );
      }

      elementosAnimacionesToggle({ checked: elementosAnimaciones }) {
        this.store.dispatch(
          new ActionAjustesCambiarAnimacionesElementos({ elementosAnimaciones })
        );
      }
    }

And this is my 'settings-container.component.html' file:

<div class="container">
  <div class="row">
    <div class="col-sm-12">
      <h1>{{ "bab.ajustes.titulo" | translate }}</h1>
    </div>
  </div>
  <br />

  <ng-container *ngIf="ajustes$ | async as ajustes">
    <div class="row">
      <div class="col-md-6 group">
        <h2>{{ "bab.ajustes.general" | translate }}</h2>
        <div class="icon-form-field">
          <mat-icon color="accent"><fa-icon icon="language" color="accent"></fa-icon></mat-icon>
          <mat-form-field>
            <mat-select
              [placeholder]="'bab.ajustes.general.placeholder' | translate"
              [ngModel]="ajustes.idioma"
              (selectionChange)="idiomaSelect($event)"
              name="language">
              <mat-option *ngFor="let i of idiomas" [value]="i.value">
                {{ "bab.ajustes.general.idioma." + i.label | translate }}
              </mat-option>
            </mat-select>
          </mat-form-field>
        </div>
        <div class="icon-form-field">
          <mat-icon color="accent"><fa-icon icon="bars" color="accent"></fa-icon></mat-icon>
          <mat-placeholder>{{ "bab.ajustes.temas.navbar-pegado" | translate }}</mat-placeholder>
          <mat-slide-toggle
            [checked]="ajustes.navbarPegado"
            (change)="navbarPegadoToggle($event)">
          </mat-slide-toggle>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-md-6 group">
        <h2>{{ "bab.ajustes.temas" | translate }}</h2>
        <div class="icon-form-field">
          <mat-icon color="accent"><fa-icon icon="paint-brush" color="accent"></fa-icon></mat-icon>
          <mat-form-field>
            <mat-select
              [placeholder]="'bab.ajustes.temas.placeholder' | translate"
              [ngModel]="ajustes.tema"
              (selectionChange)="temaSelect($event)"
              name="themes">
              <mat-option *ngFor="let t of temas" [value]="t.value">
                {{ "bab.ajustes.temas." + t.label | translate }}
              </mat-option>
            </mat-select>
          </mat-form-field>
        </div>
        <div class="icon-form-field">
          <mat-icon color="accent"><fa-icon icon="lightbulb" color="accent"></fa-icon></mat-icon>
          <mat-placeholder>{{ "bab.ajustes.temas.night-mode" | translate }}</mat-placeholder>
          <mat-slide-toggle
            [checked]="ajustes.autoNocheModo"
            (change)="autoNocheModoToggle($event)">
          </mat-slide-toggle>
        </div>
      </div>
      <div class="col-md-6 group">
        <h2>{{ "bab.ajustes.animaciones" | translate }}</h2>
        <div class="icon-form-field">
          <mat-icon color="accent"><mat-icon color="accent"><fa-icon icon="window-maximize"></fa-icon></mat-icon></mat-icon>
          <mat-placeholder>{{ "bab.ajustes.animaciones.pagina" | translate }}</mat-placeholder>
          <mat-slide-toggle
            matTooltip="Sorry, this feature is disabled in IE, EDGE and Safari"
            matTooltipPosition="before"
            *ngIf="ajustes.paginaAnimacionesDisabled"
            disabled>
          </mat-slide-toggle>
          <mat-slide-toggle
            *ngIf="!ajustes.paginaAnimacionesDisabled"
            [checked]="ajustes.paginaAnimaciones"
            (change)="paginaAnimacionesToggle($event)">
          </mat-slide-toggle>
        </div>
        <div class="icon-form-field">
          <mat-icon color="accent"><fa-icon icon="stream" color="accent"></fa-icon></mat-icon>
          <mat-placeholder>{{ "bab.ajustes.animaciones.elementos" | translate }}</mat-placeholder>
          <mat-slide-toggle
            [checked]="ajustes.elementosAnimaciones"
            (change)="elementosAnimacionesToggle($event)">
          </mat-slide-toggle>
        </div>
      </div>
    </div>
  </ng-container>
</div>

Finally, this is my 'package.json' file:

{
  "name": "bababo-web",
  "version": "1.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@agm/core": "^1.0.0-beta.5",
    "@angular/animations": "~7.2.0",
    "@angular/cdk": "^7.3.3",
    "@angular/common": "~7.2.0",
    "@angular/compiler": "~7.2.0",
    "@angular/core": "~7.2.0",
    "@angular/fire": "^5.1.1",
    "@angular/forms": "~7.2.0",
    "@angular/material": "^7.3.3",
    "@angular/platform-browser": "~7.2.0",
    "@angular/platform-browser-dynamic": "~7.2.0",
    "@angular/router": "~7.2.0",
    "@fortawesome/angular-fontawesome": "^0.3.0",
    "@fortawesome/fontawesome-svg-core": "^1.2.15",
    "@fortawesome/free-brands-svg-icons": "^5.7.2",
    "@fortawesome/free-solid-svg-icons": "^5.7.2",
    "@ngrx/effects": "^7.2.0",
    "@ngrx/router-store": "^7.2.0",
    "@ngrx/store": "^7.2.0",
    "@ngrx/store-devtools": "^7.2.0",
    "@ngx-translate/core": "^11.0.1",
    "@ngx-translate/http-loader": "^4.0.0",
    "bootstrap": "^4.3.1",
    "browser-detect": "^0.2.28",
    "core-js": "^2.5.4",
    "firebase": "^5.8.4",
    "hammerjs": "^2.0.8",
    "rxjs": "~6.3.3",
    "tslib": "^1.9.0",
    "web-animations-js": "^2.3.1",
    "zone.js": "~0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.13.0",
    "@angular/cli": "~7.3.2",
    "@angular/compiler-cli": "~7.2.0",
    "@angular/language-service": "~7.2.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "codelyzer": "~4.5.0",
    "jasmine": "^2.99.0",
    "jasmine-core": "^2.99.0",
    "jasmine-marbles": "^0.4.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "^1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "ngrx-store-freeze": "^0.2.4",
    "protractor": "~5.4.0",
    "puppeteer": "^1.12.2",
    "ts-node": "~7.0.0",
    "tslint": "~5.11.0",
    "typescript": "~3.2.2"
  }
}

This is my 'TestingModule':

import { NgModule, Injectable } from '@angular/core';
import { ComunModule } from 'src/app/comun';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import {
  Store,
  StateObservable,
  ActionsSubject,
  ReducerManager,
  StoreModule
} from '@ngrx/store';
import { BehaviorSubject } from 'rxjs';
import { RouterTestingModule } from '@angular/router/testing';

@Injectable()
export class MockStore<T> extends Store<T> {
  private stateSubject = new BehaviorSubject<T>({} as T);

  constructor(
    state$: StateObservable,
    actionsObserver: ActionsSubject,
    reducerManager: ReducerManager
  ) {
    super(state$, actionsObserver, reducerManager);
    this.source = this.stateSubject.asObservable();
  }

  setState(nextState: T) {
    this.stateSubject.next(nextState);
  }
}

export function provideMockStore() {
  return {
    provide: Store,
    useClass: MockStore
  };
}

@NgModule({
  imports: [
    NoopAnimationsModule,
    RouterTestingModule,
    ComunModule,
    TranslateModule.forRoot(),
    StoreModule.forRoot({})
  ],
  exports: [
    NoopAnimationsModule,
    RouterTestingModule,
    ComunModule,
    TranslateModule
  ],
  providers: [provideMockStore()]
})
export class TestingModule {
  constructor() {}
}

This is my 'ComunModule':

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { TranslateModule } from '@ngx-translate/core';

import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { MatTabsModule } from '@angular/material/tabs';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatChipsModule } from '@angular/material/chips';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatDividerModule } from '@angular/material/divider';
import { MatSliderModule } from '@angular/material/';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material';

import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import {
  faBars,
  faUserCircle,
  faPowerOff,
  faCog,
  faPlayCircle,
  faRocket,
  faPlus,
  faEdit,
  faTrash,
  faTimes,
  faCaretUp,
  faCaretDown,
  faExclamationTriangle,
  faFilter,
  faTasks,
  faCheck,
  faSquare,
  faLanguage,
  faPaintBrush,
  faLightbulb,
  faWindowMaximize,
  faStream,
  faBook,
  faPhoneVolume,
  faFax,
  faExternalLinkAlt,
  faUniversity,
  faAmbulance,
  faHandRock,
  faVenusMars,
  faUserFriends,
  faFileSignature,
  faHome,
  faPiggyBank,
  faUserSecret,
  faIndustry,
  faFistRaised,
  faTv
} from '@fortawesome/free-solid-svg-icons';
import {
  faGithub,
  faMediumM,
  faTwitter,
  faInstagram,
  faYoutube,
  faAngular,
  faFacebookF,
  faLinkedinIn
} from '@fortawesome/free-brands-svg-icons';

import { InicioComponent } from './inicio/inicio.component';
// import { GranEntradaComponent } from './gran-entrada/gran-entrada.component';
// import { GranEntradaAccionComponent } from './gran-entrada-accion/gran-entrada-accion.component';


library.add(
  faBars,
  faUserCircle,
  faPowerOff,
  faCog,
  faRocket,
  faPlayCircle,
  faGithub,
  faMediumM,
  faTwitter,
  faInstagram,
  faYoutube,
  faPlus,
  faEdit,
  faTrash,
  faTimes,
  faCaretUp,
  faCaretDown,
  faExclamationTriangle,
  faFilter,
  faTasks,
  faCheck,
  faSquare,
  faLanguage,
  faPaintBrush,
  faLightbulb,
  faWindowMaximize,
  faStream,
  faBook,
  faAngular,
  faFacebookF,
  faPhoneVolume,
  faFax,
  faLinkedinIn,
  faExternalLinkAlt,
  faUniversity,
  faAmbulance,
  faHandRock,
  faVenusMars,
  faUserFriends,
  faFileSignature,
  faHome,
  faPiggyBank,
  faUserSecret,
  faIndustry,
  faFistRaised,
  faTv
);

@NgModule({
  imports: [
    CommonModule,
    FormsModule,

    TranslateModule,

    MatButtonModule,
    MatToolbarModule,
    MatSelectModule,
    MatTabsModule,
    MatInputModule,
    MatProgressSpinnerModule,
    MatChipsModule,
    MatCardModule,
    MatSidenavModule,
    MatCheckboxModule,
    MatListModule,
    MatMenuModule,
    MatIconModule,
    MatTooltipModule,
    MatSnackBarModule,
    MatSlideToggleModule,
    MatDividerModule,

    FontAwesomeModule
  ],
  declarations: [InicioComponent],
  exports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,

    TranslateModule,

    MatButtonModule,
    MatMenuModule,
    MatTabsModule,
    MatChipsModule,
    MatInputModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
    MatCardModule,
    MatSidenavModule,
    MatListModule,
    MatSelectModule,
    MatToolbarModule,
    MatIconModule,
    MatTooltipModule,
    MatSnackBarModule,
    MatSlideToggleModule,
    MatDividerModule,
    MatSliderModule,
    MatDatepickerModule,
    MatNativeDateModule,

    FontAwesomeModule,

    InicioComponent,

    // GranEntradaComponent,
    // GranEntradaAccionComponent
  ]
})
export class ComunModule { }

My Project is working fine but while running unit test getting this error, Please help.

1
Could you please clearify if all of your tests are failing or only particular ones? And I was wondering, how do you import the MatSlideToggle directive? Is the TestingModule exporting that?Erbsenkoenig
It is only in this particular spec file where I have the problem, but is the only who thas uses the 'triggereventhandler' property in my app. I have an Angular Material Module ('ComunModule') that imports/exports the MatSlideToggleModule and a Test Utilities Module ('UtilsModule') that imports/exports the ComunModule. I hope I answered your question, thanks Erbsenkoenig.lbabiloni
Ah ok, yeah I was wondering how your TestBed gets to know the directive you are looking for. About which tests are failing, are all the test cases within that spec failing or just on particular test case? Just trying to figure out whether the problem might be in the setup or in one particular test case.Erbsenkoenig
@Erbsenkoenig: The six 'it' assertion are failing my test code for the same and only reason: "can not read property 'triggereventhandler' of undefined".lbabiloni
I made a stackblitz for you (stackblitz.com/edit/directive-testing) which shows, that there is probably not something wrong with your test cases but rather with the setup. Could you please show me the definition of your imported TestingModule. It seems that your test is not aware of the directive you are trying to access.Erbsenkoenig

1 Answers

0
votes

The problem is that the SlideToggle is not exported as a directive but as a component. Hence it is not accessible using the By.directive predicate.

You can access that component using the following snipped though:

 const slider = fixture.debugElement.queryAll(By.css('mat-slide-toggle'))[0];
 expect(slider).not.toBeNull();