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 }]
})
Related
Please help, i'm trying to lift up my django web page with REST API and use Angular FrontEnd where I am beginning. I have followed some tutorials on how to consume REST api and somehow I'm making a mistake there. The browser shows no errors when requesting the content but it is not coming. I appreciate every bit of help.....
here we go usluga-list.component.ts:
import { Component, OnInit } from '#angular/core';
import { Observable } from "rxjs";
import { Usluga } from "../models/usluga";
import { UslugaService } from "../services/usluga.service";
#Component({
selector: 'app-usluga-list',
templateUrl: './usluga-list.component.html',
styleUrls: ['./usluga-list.component.css']
})
export class UslugaListComponent implements OnInit {
uslugi: Observable<Usluga[]>;
constructor(private uslugaService: UslugaService) { }
ngOnInit() {
this.loadUslugiData();
}
loadUslugiData(){
this.uslugi = this.uslugaService.getAllUslugi();
then i have usluga.service.ts:
import { Injectable } from '#angular/core';
import { HttpClient } from "#angular/common/http";
import { Observable } from "rxjs";
import { Usluga } from "../models/usluga";
#Injectable({
providedIn: 'root'
})
export class UslugaService {
private endpoint ='http://localhost:8000/uslugi/';
constructor(private http: HttpClient) { }
getAllUslugi(): Observable<any>{
return this.http.get(this.endpoint)
}
getUsluga(id: number): Observable<any> {
return this.http.get(this.endpoint + id);
}
}
Then I have app-routing.ts:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { UslugaListComponent } from './usluga-list/usluga-list.component';
const routes: Routes = [
{path: '', redirectTo: 'uslugi', pathMatch: 'full'},
{path: 'uslugi', component: UslugaListComponent}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
It is odd as i get the json via localhost:8000/uslugi and it must be something wrong in the Angular:
[{"id": 1, "title": "Wizyta", "text": "Wizyta", "price": "10.99", "slug": "wizyta-p", "category": "psyc", "lekarz_id": 1}, {"id": 2, "title": "Wizyta d", "text": "Wizyta dia to...", "price": "199.30", "slug": "wiz", "category": "sek", "lekarz_id": 1}]
In order for the http call to be made you need to subscribe to it.
In your component:
this.uslugi = this.uslugaService.getAllUslugi();
// if you just want to test that it works:
this.uslugi.subscribe();
The proper way is to unsubscribe, there are many ways of doing this. This is one way that only require 'rxjs' and operators. So what you do is essentially like this:
#Component({
selector: 'app-usluga-list',
templateUrl: './usluga-list.component.html',
styleUrls: ['./usluga-list.component.css']
})
export class UslugaListComponent implements OnInit, OnDestroy {
uslugi: Observable<Usluga[]>;
unsubscribe = new Subject();
constructor(private uslugaService: UslugaService) { }
ngOnInit() {
this.loadUslugiData();
}
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
loadUslugiData(){
this.uslugi = this.uslugaService.getAllUslugi();
this.uslugi.pipe(takeUntil(this.unsubscribe)).subscribe();
}
Then that should make the http call for you
The observable variable (that stores the object from the api) name must be the same as the name in the html template, alike (flightss):
#Component({
selector: 'app-flight-list',
templateUrl: './usluga-list.component.html',
styleUrls: ['./usluga-list.component.css']
})
export class UslugaListComponent implements OnInit {
flightss: Observable<Usluga[]>;
constructor(private uslugaService: UslugaService) { }
ngOnInit() {
this.loadUslugisData();
}
loadUslugisData() {
this.flightss = this.uslugaService.getAllUslugi();
}
}
and in the template:
<tr *ngFor="let flight of flightss | async; let i=index">
<td class="text-center">{{flight.id}}</td>
<td>{{flight.title}}</td>
<td>{{flight.text}}</td>
<td>{{flight.price}}</td>
<td>{{flight.slug}}</td>
<td>{{flight.category}}</td>
<td class="td-actions text-right">
<a type="button" class="btn btn-success btn-just-icon btn-sm "
style="margin-left:10px;">
<i class="material-icons">Edit</i>
</a>
<button type="button" rel="tooltip" class="btn btn-danger btn-just-icon btn-sm" data-original-title="" title="" style="margin-left:10px;">
<i class="material-icons">Delete</i>
</button>
</td>
</tr>
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 have a custom Validator in my Model driven Forms that does validation.
Template -
<form [formGroup] = "myForm" (ngSubmit) = "save(myForm.value)">
<div>
<label>Name</label>
<input type="text" formControlName="name">
</div>
<p *ngIf="myForm.controls.name?.errors">This has to be rahulsingh!</p>
<button type = "submit" > Submit</button>
</form>
Component -
this.myForm = this.fb.group({
name: ['', [this.validateName]]
});
validateName(c: FormControl) {
return (c.value === 'rahulSingh') ? null : {
notCorrect: true
};
}
This works for Model Driven Forms
But how to use this same Validation function for Template Driven
I am following this link http://blog.thoughtram.io/angular/2016/03/21/template-driven-forms-in-angular-2.html
But i am not able to understand How to make this function Global for both forms and use it as a directive . I always end up getting a wierd error trying to achieve this.
Also One wierd thing is in my template when i try to do
<p *ngIf="myForm.hasErrors('notCorrect')">This has to be rahulsingh!</p>
i get Cannot read property 'hasError' of undefined .
To start, we'll build out the directive that will be applicable to a Template Driven Form which will automatically make it applicable to a Reactive Form (neat trick):
import { Directive, Input } from '#angular/core';
import { FormControl, NG_VALIDATORS, Validator } from '#angular/forms';
#Directive({
selector: '[requiredName][ngModel]',
providers: [{provide: NG_VALIDATORS, useExisting: SpecificNameValidatorDirective, multi: true}]
})
export class SpecificNameValidatorDirective implements Validator {
private valFn = (c: FormControl, name: string) {
return (c.value === name) ? null : {
notCorrect: true
};
}
#Input('requiredName') name: string = "temp";
constructor() {
}
validate(control: AbstractControl): {[key: string]: any} {
console.log("validation station", this.name, control.value);
return this.valFn(control, this.name);
}
}
There's a twist here in that name is not hard coded, though a default value is provided which means we can use the directive in a template driven form like so:
<form #myForm="ngForm" (ngSubmit)="save(myForm.value)">
<div>
<label>Name</label>
<input requiredName="rahulSingh" name="name" ngModel #name="ngModel" type="text" id="name">
</div>
<p *ngIf="name.errors?.notCorrect">This has to be rahulSingh!</p>
<button type="submit"> Submit</button>
</form>
Straightforward enough for the template driven form.
For the Reactive Form we can instantiate an instance of the directive:
var validator = new SpecificNameValidatorDirective();
and set the name we want
validator.name = "nobdy";
and then build our form with our instance of the directive:
this.myForm = this.fb.group({
name: ['',[validator]]
});
This will automagically look for the validate function and execute it as the various Validators all follow the Validator interface definition. The janky part is setting the name in a separate line instead of in the constructor but I have been unable to make that play nice with the template driven version.
Anyways, there's a Plunker available for you to mess around with.
You can export functions in the same way as classes:
export const validateName:ValidateFn = (c: FormControl) {
return (c.value === 'rahulSingh') ? null : {
notCorrect: true
};
}
and then import them:
import {validateName} from '...';
this.myForm = this.fb.group({
name: ['', [validateName]]
});
This tutorial might help How to Implement a Custom Validator Directive
Below given is a directive for email validation
import { Directive, forwardRef } from '#angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '#angular/forms';
#Directive({
selector: '[validateEmail]',
providers: [{ provide: NG_VALIDATORS, useExisting: forwardRef(() => ValidateEmail), multi: true }]
})
export class ValidateEmail implements Validator {
constructor() { }
validate(c: AbstractControl): { [key: string]: any } {
let EMAIL_REGEXP = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*#([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;
if (!c.value) return null;
return EMAIL_REGEXP.test(c.value) ? null : {
validEmail: true
};
}
}
below is another custom validation directive for match a model with another.
import { Directive, ElementRef, forwardRef, Attribute } from '#angular/core';
import { Validator, AbstractControl, NG_VALIDATORS, FormControl } from '#angular/forms';
#Directive({
selector: '[matchControl][ngModel]',
providers: [{ provide: NG_VALIDATORS, useExisting: forwardRef(() => MatchControl), multi: true }]
})
export class MatchControl implements Validator{
constructor(#Attribute('matchControl') private matchControl: string) { }
validate(c: AbstractControl): { [key: string]: any; } {
let v = c.value;
let e = c.root.get(this.matchControl)
return (e && v !== e.value) ? {
match: true
} : null;
}
}
and its html as follows
<input name="password" type="password" class="form-control" required minlength="8" maxlength="15" [(ngModel)]="user.password" #password="ngModel">
<input name="cpassword" type="password" class="form-control" required [(ngModel)]="user.cpassword" #cpassword="ngModel" matchControl="password">
hope these two examples will help you
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');
})));
});
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');