我在angular7应用程序中有一个“设置”模块,它使用Ngrx库。我的目标是统一测试启动它的组件模板。使用Angular Material的MatSlideToggle功能时,可以通过单击模态组件来交替打开/关闭控件。但是,Karma~3.1.1和Jasmine ^ 2.99.0测试的结果将事件返回为undefined。我得到错误“无法读取未定义的属性'triggereventhandler'”。
注意:我真诚地感谢Tomas Trajan先生(@tomastrajan)和开放项目"Angular NgRx Material Starter"的其他贡献者。
我在第一个问题中提前感谢你的关注。
这是我的'ajustes-cont.component.spec.ts'文件:
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);
});
});
这是我的'settings-container.component.ts'文件:
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 })
);
}
}
这是我的'settings-container.component.html'文件:
<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>
最后,这是我的'package.json'文件:
{
"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"
}
}
这是我的'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() {}
}
这是我的'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 { }
我的项目工作正常但在运行单元测试时遇到此错误,请帮忙。
问题是SlideToggle不是作为指令导出的,而是作为组件导出的。因此,使用By.directive谓词无法访问它。
您可以使用以下剪切访问该组件:
const slider = fixture.debugElement.queryAll(By.css('mat-slide-toggle'))[0];
expect(slider).not.toBeNull();