I have login function inside my LoginComponent:
login() {
this.loading = true;
this.subscription = this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
this.em.changeNav(1);
this.loading = false;
this.Auth.setToken(result);
this.router.navigate(['/code']);
this.subscription.unsubscribe();
},
err => {
this.error = JSON.parse(err._body).error;
this.loading = false;
});
}
this.authenticationService.login is the service which send http request to api...
Here is the test:
it('should login', fakeAsync(() => {
spyOn(component, 'login');
let button = fixture.debugElement.nativeElement.querySelector('button');
button.click();
//CHECK IF LOGIN FUNCTION CALLED
fixture.whenStable().then(() => {
expect(component.login).toHaveBeenCalled();
})
}));
How can I mock this.authenticationService.login service and assert things in subscribe method?
EDIT
Test:
import { async, ComponentFixture, TestBed, fakeAsync, tick, inject } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { RouterTestingModule } from '#angular/router/testing';
import {Router} from '#angular/router';
import { Http, Request, RequestOptionsArgs, Response, XHRBackend, RequestOptions, ConnectionBackend, Headers, HttpModule, BaseRequestOptions } from '#angular/http';
import {LoginService} from './login.service';
import {
MockBackend,
MockConnection
} from '#angular/http/testing';
import {EmitterService} from '../emitter.service';
import {AuthTokenService} from '../auth-token.service';
import { LoginComponent } from './login.component';
import {Observable} from 'rxjs';
describe('LoginComponent', () => {
let backend: MockBackend;
let service: LoginService;
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
class LoginServiceStub {
login() { }
};
class RouterStub {
navigate(url: string) { return url; }
}
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [
FormsModule,
HttpModule,
ReactiveFormsModule,
RouterTestingModule
],
providers: [
LoginService,
EmitterService,
AuthTokenService,
{ provide: LoginService, useClass: LoginServiceStub },
// { provide: Router, useClass: RouterStub }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('Should log in and navigate to dashboard', fakeAsync(inject([LoginService, Router], (authService: LoginService, router: Router) => {
spyOn(component, 'login');
let button = fixture.debugElement.nativeElement.querySelector('button');
spyOn(authService, 'login').and.returnValue(Observable.of(true));
button.click();
tick();
expect(component.login).toHaveBeenCalled();
expect(component.loading).toBe(false);
})));
});
Problem with this is login function from component is never called When I console.log inside login method in component it display message...
This is Html part:
<form name="form" class="form-horizontal" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
<img class="loading-img" *ngIf="loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username" class="cols-sm-2 control-label">Email</label>
<div class="cols-sm-10">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user fa" aria-hidden="true"></i></span>
<input type="text" class="form-control" name="username" placeholder="Your email" [(ngModel)]="model.username" #username="ngModel" required />
</div>
</div>
<div *ngIf="f.submitted && !username.valid" class="help-block">Email is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password" class="cols-sm-2 control-label">Password</label>
<div class="cols-sm-10">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock fa-lg" aria-hidden="true"></i></span>
<input type="password" placeholder="Your password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel" required />
</div>
</div>
<div *ngIf="f.submitted && !password.valid" class="help-block">Password is required</div>
</div>
<div class="form-group">
<button id="login" type="submit" class="btn btn-primary">Login</button>
<div *ngIf="error" style="margin-top: 20px;" class="text-center alert alert-danger">{{error}}</div>
</div>
<div class="form-group text-center login-down" >
<a routerLink="/register" routerLinkActive="active">Register now</a>
<a routerLink="/forgot" routerLinkActive="active">Forgot password</a>
</div>
</form>
You can create mock class for service:
class AuthenticationServiceStub {
login() {}
};
then provide it in configureTestingModule:
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationServiceStub },
{ provide: Router, useClass: RouterStub }
]
})
inject in your test
inject([AuthenticationService, Router],
(authService: AuthenticationService, router: Router) =>
wrap it in async(+whenStable) or fakeAsync(+tick) or use jasmine.done directly for waiting execution of async methods
it('Should log...', fakeAsync(inject([AuthenticationService, Router]
and mock login method like:
spyOn(authService, 'login').and.returnValue(Observable.of(true) );
Plunker Example
Here is the entire spec:
describe('Welcome component tests', () => {
let comp: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
class AuthenticationServiceStub {
login() {}
};
class RouterStub {
navigateByUrl(url: string) { return url; }
}
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationServiceStub },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents()
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('.login'));
el = de.nativeElement;
fixture.detectChanges();
});
it('Should log in and navigate to dashboard', fakeAsync(inject([AuthenticationService, Router], (authService: AuthenticationService, router: Router) => {
const spy = spyOn(router, 'navigateByUrl');
spyOn(authService, 'login').and.returnValue(Observable.of(true) );
el.click();
tick();
const navArgs = spy.calls.first().args[0];
expect(navArgs).toBe('/dashboard');
})));
});
Related
I try to test a template driven form in Ionic v4.
But I can`t find a way to get the input element in the ion-input element.
This is what I tried:
login-register.page:
import { GroupsService } from './../../services/groups.service';
import { AuthenticateService, RegisterLoginReturnMessage } from './../../services/authenticate.service';
import { Component, OnInit, OnDestroy, ViewChild } from '#angular/core';
import { ActivatedRoute, Router, NavigationExtras } from '#angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AlertController, ToastController } from '#ionic/angular';
import { TranslateService } from '#ngx-translate/core';
import { Location } from '#angular/common';
import { NavController } from '#ionic/angular';
import { addslashes } from './../../helpers';
import { FCM } from '#ionic-native/fcm/ngx';
import { StorageService } from 'src/app/services/storage.service';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-login-register',
templateUrl: './login-register.page.html',
styleUrls: ['./login-register.page.scss'],
})
export class LoginRegisterPage implements OnInit, OnDestroy {
#ViewChild('loginRegisterForm', { static: true })loginRegisterForm: NgForm;
navParams: any;
email: string;
userName = 'blafasel';
firstName: string;
lastName: string;
password: string;
passwordreset: string;
loading = false;
emailRegExpr = '[a-zA-Z]{1}[a-zA-Z0-9.-_]*#[a-zA-Z0-9]{1}[a-zA-Z0-9.-]*[a-zA-Z0-9]{1}[.][a-zA-Z]{2,}';
gdprChecked = false;
private unsubscribe: Subject<void> = new Subject();
constructor(
private route: ActivatedRoute,
private router: Router,
private authenticateService: AuthenticateService,
public alertController: AlertController,
public translateService: TranslateService,
private location: Location,
private navCtrl: NavController,
private groupsService: GroupsService,
private toastController: ToastController,
private fcm: FCM,
private storageService: StorageService
) {
this.route.queryParams.subscribe(params => {
if (this.router.getCurrentNavigation().extras.state) {
console.log('if (this.router.getCurrentNavigation().extras.state)');
this.navParams = this.router.getCurrentNavigation().extras.state;
console.log(this.navParams);
}
});
}
ngOnInit() {}
ionViewWillEnter() {
console.log('login is entered');
}
submitForm(): void {
this.loading = true;
console.log('submitform');
if (this.navParams.mode === 'register') {
this.authenticateService
.registerUserAndGetMail(this.userName, this.email, this.firstName, this.lastName)
.pipe(takeUntil(this.unsubscribe))
.subscribe(
(response: RegisterLoginReturnMessage) => {
this.loading = false;
if (response.valid === true) {
this.navCtrl.navigateRoot('');
this.presentAlert('success', 'registerSuccess');
} else {
this.presentAlert('error', response.message[0], response.message[1] ? response.message[1] : '');
}
console.log('response:', response);
},
(error: RegisterLoginReturnMessage) => {
this.loading = false;
this.presentAlert('error', error.message[0], error.message[1] ? error.message[1] : '');
console.log('error bla:', error);
}
);
}
if (this.navParams.mode === 'login') {
this.authenticateService
.getUserToken(this.userName, addslashes(this.password))
.then((response: RegisterLoginReturnMessage) => {
this.loading = false;
// this.location.back();
// this.presentAlert('success', response.message[0], response.message[1] ? response.message[1] : '');
this.presentToast('success', response.message[0], response.message[1] ? response.message[1] : '');
// Fetch Groups from backend
this.groupsService.fetchUserData().then(data => {
console.log('promise all data:', data);
this.fcm
.subscribeToTopic(data[2].id)
.then(() => {
console.log('succesfullysubscribe:', data[2].id);
})
.catch(err => {
console.error('error subscription:', err);
});
});
this.navCtrl.navigateRoot('');
console.log('response:', response);
})
.catch((error: RegisterLoginReturnMessage) => {
this.loading = false;
if (error.status === 0) {
this.presentAlert('error', 'wrongConnectionWp');
} else if (error.status === 403) {
this.presentAlert('error', 'wrongLoginData');
} else {
this.presentAlert('error', error.status.toString(), error.statusText);
}
console.log('error bla:', error);
});
}
if (this.navParams.mode === 'passwordReset') {
this.authenticateService
.resetPassword(this.passwordreset)
.then(response => {
this.loading = false;
this.presentAlert('success', response.message);
this.navCtrl.navigateRoot('');
})
.catch(error => {
this.loading = false;
this.presentAlert('error', error.message[0]);
console.log('error in reseet pass catch', error);
});
}
}
presentAlert(header: string, message: string, messageTwo?: string): void {
this.translateService.get([header, message, messageTwo ? messageTwo : '', 'OK']).subscribe(async (res: string[]) => {
const newAlert = await this.alertController.create({
header: res[header],
message: messageTwo ? res[message] + res[messageTwo] : res[message],
buttons: [
{
text: 'OK',
handler: () => {
console.log('ok pressed');
},
},
],
});
await newAlert.present();
});
}
async presentToast(title: string, message: string, message2: string = '', duration?: number): Promise<void> {
this.translateService.get([title, message, message2, 'OK']).subscribe(async (res: string[]) => {
const toast = await this.toastController.create({
header: res[title],
message: message2 ? `${res[message]}<br>${res[message2]}` : `${res[message]}`,
position: 'bottom',
duration: duration ? duration : 3000,
showCloseButton: true,
});
toast.present();
});
}
showPassword(passwordInput) {
passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password';
}
gotToGdpr() {
const bla = this.translateService.currentLang;
console.log('gotogdpr clkicked:', this.translateService.currentLang);
const navigationExtras: NavigationExtras = {
state: {
postId:
this.translateService.currentLang === 'de' ||
this.translateService.currentLang === 'de-AT' ||
this.translateService.currentLang === 'de-CH' ||
this.translateService.currentLang === 'de-DE' ||
this.translateService.currentLang === 'de-LI'
? this.storageService.appData.gdprId.de
: this.storageService.appData.gdprId.en,
},
};
this.router.navigateByUrl('posts', navigationExtras);
}
ngOnDestroy(): void {
// For unsubscribing all Subscriptions
console.log('ngOnDestory');
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
login-register.page.html:
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="{{ 'back' | translate }}"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="navParams.mode === 'login'">{{ 'login' | translate }}</ion-title>
<ion-title *ngIf="navParams.mode === 'register'">{{ 'register' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form #loginRegisterForm="ngForm" (ngSubmit)="submitForm()">
<ion-list inset>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'email' | translate"
name="email"
id="emailField"
type="text"
required
[(ngModel)]="email"
[pattern]="emailRegExpr"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'username' | translate"
name="userName"
id="userNameField"
type="text"
required
[(ngModel)]="userName"
pattern="[a-zA-Z0-9]{4,30}"
></ion-input>
</ion-item>
<div class="username-message" item-content *ngIf="navParams.mode === 'register'">
{{ 'usernameRestrictions' | translate }}
</div>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'firstName' | translate"
name="firstName"
id="firstNameField"
type="text"
required
[(ngModel)]="firstName"
pattern="[a-zA-Z0-9\-\s]{1,100}"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'lastName' | translate"
name="lastName"
id="userNameField"
type="text"
required
[(ngModel)]="lastName"
pattern="[a-zA-Z0-9\-\s]{1,100}"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'login'">
<ion-input
[placeholder]="'username' | translate"
name="userName"
id="userNameField"
type="text"
required
[(ngModel)]="userName"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'login'">
<ion-input
#passwordInput
[placeholder]="'password' | translate"
name="password"
id="passwordField"
type="password"
required
[(ngModel)]="password"
></ion-input>
<ion-icon
*ngIf="passwordInput.type === 'password'"
slot="end"
name="eye"
(click)="showPassword(passwordInput)"
style="font-size: 1.7rem;z-index:10"
></ion-icon>
<ion-icon
*ngIf="passwordInput.type == 'text'"
slot="end"
name="eye-off"
(click)="showPassword(passwordInput)"
style="font-size: 1.7rem;z-index:10"
></ion-icon>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'passwordReset'">
<ion-input name="passwordreset" id="passwordreset" type="text" required [(ngModel)]="passwordreset"></ion-input>
</ion-item>
<div class="username-message" item-content *ngIf="navParams.mode === 'passwordReset'">
{{ 'passwordReset' | translate }}
</div>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<a (click)="gotToGdpr()">{{ 'privacyPolicyLink' | translate }}</a>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
{{ 'gdprHint' | translate }}
<ion-checkbox pattern="true" name="gdprChecked" slot="end" [(ngModel)]="gdprChecked"></ion-checkbox>
</ion-item>
</ion-list>
<ion-row>
<ion-col>
<ion-button size="full" color="tertiary" type="submit" [disabled]="!loginRegisterForm.form.valid">{{
'submit' | translate
}}</ion-button>
</ion-col>
</ion-row>
</form>
<ion-spinner *ngIf="loading" class="spinner-large spinner-center" color="secondary"></ion-spinner>
</ion-content>
login-register.page.spec.ts:
import { AuthenticateService } from './../../services/authenticate.service';
import { IonicStorageModule } from '#ionic/storage';
import { CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { async, ComponentFixture, TestBed, inject } from '#angular/core/testing';
import { LoginRegisterPage } from './login-register.page';
import { TranslateModule, TranslateLoader } from '#ngx-translate/core';
import { HttpClientModule, HttpClient } from '#angular/common/http';
import { createTranslateLoader } from 'src/app/app.module';
import { FormsModule } from '#angular/forms';
import { IonicModule } from '#ionic/angular';
import { RouterTestingModule } from '#angular/router/testing';
import { FCM } from '#ionic-native/fcm/ngx';
import { FCMMock } from 'src/mocks/fcmMock';
import { Router } from '#angular/router';
import { By } from '#angular/platform-browser';
// test environment
const testModuleConfig = () => {
TestBed.configureTestingModule({
declarations: [LoginRegisterPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [{ provide: FCM, useClass: FCMMock }, AuthenticateService],
imports: [
FormsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
RouterTestingModule.withRoutes([]),
IonicModule.forRoot(),
IonicStorageModule.forRoot(),
HttpClientModule,
],
}).compileComponents();
};
describe('LoginRegisterPage', () => {
let component: LoginRegisterPage;
let fixture: ComponentFixture<LoginRegisterPage>;
let router: jasmine.SpyObj<Router>;
let service: AuthenticateService;
beforeEach(async(() => {
testModuleConfig();
}));
beforeEach(inject([AuthenticateService], (s) => {
service = s;
router = TestBed.get(Router);
spyOn(router, 'getCurrentNavigation').and.returnValue({ extras: { state: { mode: 'login' } } });
fixture = TestBed.createComponent(LoginRegisterPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
afterEach(() => {
fixture.destroy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should be navparams login', () => {
expect(component.navParams).toEqual({ mode: 'login' });
});
it('form invalid when empty', () => {
console.log('component.loginRegisterForm.form', component.loginRegisterForm.form);
expect(component.loginRegisterForm.form.valid).toBeFalsy();
});
it('should contain the correct title', () => {
// component.title = 'Test';
fixture.detectChanges();
// const element = fixture.debugElement.nativeElement.querySelector('#userNameField');
const element = fixture.debugElement.query(By.css('#userNameField input'));
console.log('element+++++++++++++++++', element);
console.log('element+++++++++.textContent++++++++', element.textContent);
const inputs = fixture.debugElement.queryAll(By.css('input'));
console.log('first input element+++++++++++++++++', inputs[0]);
// expect(element.i).toEqual('blafasel');
});
it('test function should return bla', () => {
expect(service.testTestFunction()).toBe('bla');
});
});
The
console.log('element+++++++++++++++++', element);
gives null. But the ion-input element is there with
fixture.debugElement.query(By.css('#userNameField'));
Can somebody help me here to make the input element available in the tests?
Thanx a lot.
You may use the #ViewChild angular decorator to get a reference to the ionic input element into a class instance variable.
For example, in your sample code:
<ion-input
#emailField
[placeholder]="'email' | translate"
name="email"
id="emailField"
type="text"
required
[(ngModel)]="email"
[pattern]="emailRegExpr"></ion-input>
In LoginRegisterPage, you can declare a member as:
#ViewChild('emailField', {static: false}) emailField: IonInput;
You can then access the control's angular component methods directly from the emailField member variable.
HTH.
I need to validate phone number field in form using regex in template driven form in angular. This is what i have tried
addcontact.component.html
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="text" class="form-control" id="phone" name="phone" required [(ngModel)]="contact.phone"
#phone="ngModel" appPhonevalidator />
<div *ngIf="phone.invalid && (phone.dirty || phone.touched)" class="alert alert-danger">
<div *ngIf="phone.errors.required">
Phone number is required.
</div>
<div *ngIf="phone.errors?.phoneNumberValid">
Phone number is not valid.
</div>
</div>
</div>
phonevalidator.directive.ts
import { Directive } from '#angular/core';
import { Validator, AbstractControl } from '#angular/forms';
import { ValidatorService } from '../_services/validator.service';
import { Observable } from 'rxjs';
#Directive({
selector: '[appPhonevalidator]'
})
export class PhonevalidatorDirective implements Validator {
constructor(private validateService: ValidatorService) {}
validate(control: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> {
console.log(control.value)
return this.validateService.validatePhoneNumber(control.value)
}
}
validator.service.ts
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ValidatorService {
constructor() { }
validatePhoneNumber(phone) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const pattern = new RegExp('^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[()-\s\./0-9]*$');
if (pattern.test(phone)) {
resolve({ phoneNumberValid: true })
} else {
resolve({ phoneNumberValid: false })
}
}, 1000);
})
}
}
Its not even going to the directive as expected. Whats wrong in here? I added the directive in declaration and providers in app.module.ts(entry module) (Only giving relevant code here)
app.module.ts
#NgModule({
declarations: [
PhonevalidatorDirective
],
providers: [{
provide: NG_VALIDATORS,
useClass: PhonevalidatorDirective,
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule { }
UPDATE
After giving the providers in directive it worked, don't know the reason though
#Directive({
selector: '[appPhonevalidator]',
providers: [{ provide: NG_VALIDATORS, useExisting: forwardRef(() => PhonevalidatorDirective), multi: true }]
})
I'm trying to test if click event is properly handled. I'm trying to trigger click event on a button and check if proper call on router was executed.
Don't know why stubRouteSpy did not registered call to navigate (last expect fails)
Template code:
<div class="col-lg-3 col-md-4 col-xs-6 thumb project" *ngFor="let project of projects">
<div class = "thumbnail">
<img class="img-responsive img-rounded" src="{{project.imgUrl}}" alt="">
<div class = "caption">
<h4 id='titleHeader'>{{project.title}}</h4>
<div class="btn-group" role="group" style="all">
<button class="btn btn-primary">Order</button>
<button id='customizeButton' class="btn btn-info" (click)="onCustomize(project.id)">Customize</button>
</div>
</div>
</div>
Component code:
public errorMessage = '';
public projects = [];
constructor(private router: Router, private projectListService: ProjectListService) {
}
public ngOnInit() {
this.getProjects();
}
public getProjects() {
this.projectListService.getAllProjects()
.subscribe(
(projects) => this.projects = projects,
(error) => this.errorMessage = <any> error);
}
public onCustomize(id: string) {
console.log(id);
let navigate = this.router.navigate(['design', id]);
}
Spec code:
describe('GalleryComponent (inline template)', () => {
let comp: GalleryComponent;
let fixture: ComponentFixture<GalleryComponent>;
let projectListService: ProjectListService;
let spy: jasmine.Spy;
let de: DebugElement[];
let stubRoute: Router;
let stubRouteSpy: jasmine.Spy;
beforeEach(() => {
stubRoute = <Router> { navigate: ([]) => {}};
TestBed.configureTestingModule({
declarations: [GalleryComponent],
providers: [
ProjectListService,
{provide: Router, useValue: stubRoute},
{provide: Http, useValue: {}}
],
});
fixture = TestBed.createComponent(GalleryComponent);
comp = fixture.componentInstance;
// ProjectListService actually injected into the component
projectListService = fixture.debugElement.injector.get(ProjectListService);
// Setup spy on the `getAllProjects` method
let fakeProjects = [new Project(1, 'title1', ''), new Project(2, 'title2', '')];
spy = spyOn(projectListService, 'getAllProjects')
.and.returnValue(Observable.of<Project[]>(fakeProjects));
stubRouteSpy = spyOn(stubRoute, 'navigate');
});
it('should navigate to designer when customize button clicked', async(() => {
fixture.detectChanges(); // init
fixture.whenStable().then(() => { // wait for async getAllProjects
fixture.detectChanges(); // update view with projects
fixture.nativeElement.querySelectorAll('#customizeButton')[0].click();
expect(fixture.nativeElement.querySelectorAll('#customizeButton').length).toBe(2); // this pass
fixture.detectChanges();
expect(stubRouteSpy.calls.any()).toBe(true, 'navigate called'); // this is false
});
}));
});
According the documentation here, you should get the provided RouterStub from the injector.
I suggest you use the example in the documentation.
the stub class:
class RouterStub {
navigateByUrl(url: string) { return url; }
}
provide:
{ provide: Router, useClass: RouterStub }
get injector:
routerStub = fixture.debugElement.injector.get(RouterStub);
After reading this guide I've decided to test my simple login page which contains just 2 input boxes and a submit button. The component then uses a LoginService to pass these data to backend.
( Also note that I'm new to Unit testing as such, so I'm not sure if this is a good approach as how to test such component. )
For starters, I only wanted to check, if the initial value of #username input element is empty. But I couldn't even make the spec to work due to the below reported issues:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Failed: Unexpected value 'Http' imported by the module 'DynamicTestModule'
Error: Unexpected value 'Http' imported by the module 'DynamicTestModule'
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.348 secs)
When I tried deleting the Http module, I got this error :
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Error: DI Error
Error: Uncaught (in promise): Error: No provider for Http!
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.456 secs)
login.component.html
<div class="login jumbotron center-block">
<h1>Login</h1>
<form (ngSubmit)="onSubmit($event)" #loginForm="ngForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" [(ngModel)]="model.username" name="username"
placeholder="Username" #username="ngModel" required>
<div [hidden]="username.valid || username.pristine" class="alert alert-danger"> Username is required </div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" [(ngModel)]="model.password" name="password" placeholder="Password" #password="ngModel" required>
<div [hidden]="password.valid || password.pristine" class="alert alert-danger"> Password is required </div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!loginForm.form.valid" >Submit</button>
<a [routerLink]="['/signup']">Click here to Signup</a>
</form>
</div>
login.component.ts
import { Component } from '#angular/core';
import { Router } from '#angular/router';
import { LoginService } from '../services/login.service';
import { User } from '../extensions/user.class';
#Component({
moduleId: module.id,
selector: 'login',
templateUrl: '../templates/login.component.html',
styleUrls: [ '../styles/login.component.css' ],
providers: [ LoginService ]
})
export class LoginComponent {
private submitted = false;
private model = new User();
constructor(
private router: Router,
private loginService: LoginService
) {}
public onSubmit(event: any): void {
event.preventDefault();
if ( ! this.submitted ) {
this.submitted = true;
if ( this.model.username && this.model.password ) {
this.loginService.login(this.model).then( (token) => {
localStorage.setItem('id_token', token.id);
this.router.navigate(['home']);
}).catch( (error) => this.onLoginFailed(error) );
} else {
console.warn('No username or password provided');
}
}
}
private onLoginFailed( error: any ): void {
//// errors are already handled in login-service ////
console.error(error);
this.submitted = false; /// reset form submit funcitonality ///
}
public signup(event: any): void {
event.preventDefault();
this.router.navigate(['signup']);
}
}
login.component.spec.ts
import { async } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { RouterTestingModule } from '#angular/router/testing';
import { Component } from '#angular/core';
import { Location } from '#angular/common';
import { LoginComponent } from './login.component';
import { LoginService } from '../services/login.service';
import { Http } from '#angular/http';
import { User } from '../extensions/user.class';
#Component({
template: ''
})
class DummyComponent{}
class LoginServiceStub {
login( user: User ){
return true;
}
}
describe('LoginComponent', () => {
let comp: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let de: DebugElement;
let el: HTMLElement;
let location: Location;
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
}).compileComponents() // compile template and css
.then( () => {
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance; // LoginComponent test instance
de = fixture.debugElement.query(By.css('input[name="username"]'));
el = de.nativeElement;
});
}));
it('Username field should be empty', () => {
fixture.detectChanges();
expect(el.textContent).toContain('');
});
});
The problem is that the LoginService is declared at the component level
#Component({
providers: [ LoginService ]
})
This will supersede any same service declared at the module level, which is where you declare the mock in the test. There are a couple things you can do:
Don't declare the service at the component level. If there is not good reason to scope it to the component, then just declare it at the #NgModule.providers and make it a singleton.
Override the #Component.providers in the test.
TestBed.configureTestingModule({})
TestBed.overrideComponent(LoginComponent, {
set: {
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
]
}
});
Alexus,
Have you tried importing the Http module into your test component and adding it to the "providers" array? I think you would have to specify all of your dependencies in this case. I'm assuming your LoginService is requiring {Http} as a provision, but your test component does not register {Http} so it can't find an instance to use.
EDIT:
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub },
Http,
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
DOUBLE EDIT!:
In addition, you'll probably want to mock out the Http module, as you won't actually want to send a request during your unit test. "MockBackend" from #angular/http/testing is sufficient for this -- in this case, you would want to use the "provide" syntax you uses with the Login Service to provide an Http module that uses the MockBackend to generate responses.
I'm trying to run Angular 2 unit tests on an Angular 2 Component with Jasmine (I am not using Karma, however... just webpacking my code then running the tests in the default Jasmine SpecRunner.html).
When I run my code, I get the error: "A platform with a different configuration has been created. Please destroy it first." Been banging my head on this all day. Reading every post on StackOverflow I can find, but I'm still stuck. Any suggestions?
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, async, fakeAsync, tick } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "#angular/platform-browser-dynamic/testing";
import {AppLogin} from "../../../app/login/app.login";
describe("Login Component", () => {
let comp: AppLogin;
let fixture: ComponentFixture<AppLogin>;
let el: DebugElement;
function setup() {
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
}
setup();
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppLogin]
});
fixture = TestBed.createComponent(AppLogin);
comp = fixture.componentInstance;
});
it("login form should pass validation", () => {
fixture.detectChanges();
var form = {
EmailAddress: 'test#me.com',
Password: 'test'
};
var validated = comp.formValidated(form);
expect(validated).toBe(true);
});
});
Here is the component I'm attempting to test...
import { Component } from '#angular/core';
#Component({
selector: 'app-login',
template: `
<form *ngIf="active" (ngSubmit)="onSubmit()" class="form-signin">
<h2 class="form-signin-heading">Please sign in</h2>
<label for="EmailAddress" class="sr-only">Email address</label>
<input type="email" name="EmailAddress" id="EmailAddress" class="form-control" placeholder="Email address"
[(ngModel)]="form.EmailAddress" required autofocus>
<label for="Password" class="sr-only">Password</label>
<input type="password" name="Password" id="Password" class="form-control" placeholder="Password" required
[(ngModel)]="form.Password">
<div class="checkbox">
<label>
<input type="checkbox" id="RememberMe" value="remember-me" [(ngModel)]="form.RememberMe"> Remember me
</label>
</div>
<div *ngIf="form.hasError">
<div *ngFor="let error of form.errorMessages" class="alert alert-danger fade in">{{error.message}}</div>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
`
})
export class AppLogin {
form: any;
constructor() {
//
}
formValidated(form: any): boolean {
form.errorMessages = [];
form.hasError = false;
if (form.EmailAddress == null)
form.errorMessages.push({ message: 'Email Address is required.' });
if (form.Password == null)
form.errorMessages.push({ message: 'Password is required.' });
if (form.errorMessages.count > 0)
form.hasError = true;
return !form.hasError;
}
onSubmit(form: any): void {
console.log('Form data: ', form);
}
}
Unfortunately, Jasmine alone did not provide me with the debug information I needed, so I am no longer using Jasmine alone for my unit testing. I am using the recommended Karma/Jasmine setup. (NOTE: However, I am not using the Angular karma-test-shim, which is why I have to run TestBed.initTestEnvironment).
I ran the tests in Karma and I got an error about my component's template. My component template has an angular form. I had to import the angular FormsModule into my test environment. Here is the code which resolved the issue...
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, async, fakeAsync, tick } from '#angular/core/testing';
import { By, BrowserModule } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "#angular/platform-browser-dynamic/testing";
import { FormsModule } from '#angular/forms';
import {AppLogin} from "../../../app/login/app.login";
describe("Login Component", () => {
let comp: AppLogin;
let fixture: ComponentFixture<AppLogin>;
let el: DebugElement;
beforeEach(() => {
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
TestBed.configureTestingModule({
imports: [ FormsModule, BrowserModule ],
declarations: [ AppLogin ]
});
fixture = TestBed.createComponent(AppLogin);
comp = fixture.componentInstance;
});
it("login form should pass validation", () => {
fixture.detectChanges();
var form = {
EmailAddress: 'test#me.com',
Password: 'test'
};
var validated = comp.formValidated(form);
expect(validated).toBe(true);
});
});
I had a bunch of trouble setting up Karma with Webpack originally, but here is a Karma config I wrote, which is working really well for me (and doesn't require the karma-test-shim)...
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'src/tests/tests.ts',
'src/tests/login/app.login.spec.ts'
],
exclude: [
],
preprocessors: {
'src/tests/tests.ts': ['webpack'],
'src/tests/login/app.login.spec.ts': ['webpack', 'sourcemap']
},
webpack: {
devtool: 'inline-source-map',
resolve: {
extensions: ['', '.ts', '.js']
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
},
{
test: /\.ts$/,
loaders: ['ts-loader']
}
]
}
},
webpackMiddleware: {
// webpack-dev-middleware configuration
noInfo: true
},
plugins: [
require("karma-webpack"),
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-sourcemap-loader"),
require("karma-spec-reporter")
],
reporters: ['spec'],
port: 9876,
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true
});
};
And finally, here's the code for the tests.ts file I included in my Karma config. This is where I require() all the code I need to run angular tests...
require('zone.js/dist/zone');
require('reflect-metadata');
require('rxjs');
require('#angular/platform-browser');
require('#angular/platform-browser-dynamic');
require('#angular/core');
require('#angular/common');
require('#angular/http');
require('#angular/router');
Error.stackTraceLimit = Infinity;
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy'); // since zone.js 0.6.15
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var testing = require('#angular/core/testing');