Angular2 trigger button click in unit test and verify event handling - unit-testing

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

Related

map is not a function

I am trying to make an website that connets with a weather api and gets some info about current weather in given city. When i looked at network flow in my application i do recive a json that contais that information but i cannot map it and display results. Error i revice is :
TypeError: response.json(...).map is not a function WeatherSearchComponent.ts:138
at MapSubscriber.project (WeatherSearchComponent.ts:84)
at MapSubscriber._next (map.js:77)
at MapSubscriber.Subscriber.next (Subscriber.js:89)
at XMLHttpRequest.onLoad (http.umd.js:1083)
at ZoneDelegate.invokeTask (zone.js:225)
at Object.onInvokeTask (core.umd.js:6004)
at ZoneDelegate.invokeTask (zone.js:224)
at Zone.runTask (zone.js:125)
at XMLHttpRequest.ZoneTask.invoke (zone.js:293)
Heres WeatherSearchComponent:
import {
Component,
Injectable,
OnInit,
ElementRef,
EventEmitter,
Inject
} from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs';
import 'rxjs/Rx';
import 'rxjs/add/operator/map';
export var WEATHER_API_KEY: string = 'api_key';
export var WEATHER_API_URL: string ='http://api.openweathermap.org/data/2.5/weather';
export var GDANSK_ID: string = '3099434';
/*let loadingGif: string = ((<any>window).__karma__) ? '' :
require('images/loading.gif');*/
class SearchResult {
content: string;
constructor(obj?: any) {
this.content= obj && obj.content ||
WEATHER_API_URL + 'id=' + obj.city + 'appid=' + WEATHER_API_KEY;
}
}
// http://api.openweathermap.org/data/2.5/weather?
// id=3099434&appid=api_key
#Injectable()
export class WeatherService {
constructor(private http: Http,
#Inject(WEATHER_API_KEY) private apiKey: string,
#Inject(WEATHER_API_URL) private apiUrl: string) {
}
search(city: string): Observable<SearchResult[]> {
let params: string = [
`q=${city}`,
`appid=${this.apiKey}`
].join('&');
let queryUrl: string = `${this.apiUrl}?${params}`;
console.log("query url" , queryUrl);
var getRequest = this.http.get(queryUrl);
console.log("getRequest", getRequest);
return getRequest
.map((response: Response) => {
return (<any>response.json()).map(item => {
console.log("raw item", item); // uncomment if you want to debug
return new SearchResult({
content: item.main.temp
});
});
});
}
}
export var weatherServiceInjectables: Array<any> = [
{provide: WeatherService, useClass: WeatherService},
{provide: WEATHER_API_KEY, useValue: WEATHER_API_KEY},
{provide: WEATHER_API_URL, useValue: WEATHER_API_URL}
];
/**
* SearchBox displays the search box and emits events based on the results
*/
#Component({
outputs: ['loading', 'results'],
selector: 'search-box',
template: `
<input type="text" class="form-control" placeholder="Search" autofocus>
`
})
export class SearchBox implements OnInit {
loading: EventEmitter<boolean> = new EventEmitter<boolean>();
results: EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]> ();
constructor(private weather: WeatherService,
private el: ElementRef) {
}
ngOnInit(): void {
// convert the `keyup` event into an observable stream
var observable =
Observable.fromEvent(this.el.nativeElement, 'keyup');
observable
.map((e: any) => e.target.value) // extract the value of the input
.filter((text: string) => text.length > 1) // filter out if empty
.debounceTime(250) // only once every 250ms
.do(() => this.loading.next(true)) // enable loading
// search, discarding old events if new input comes in
.map((query: string) => this.weather.search(query))
.switch()
// act on the return of the search
.subscribe(
(results: SearchResult[]) => { // on sucesss
this.loading.next(false);
this.results.next(results);
},
(err: any) => { // on error
console.log(err);
this.loading.next(false);
},
() => { // on completion
this.loading.next(false);
}
);
}
}
#Component({
inputs: ['result'],
selector: 'search-result',
template: `
<div class="col-sm-6 col-md-3">
<div class="thumbnail">
<img src="{{result.thumbnailUrl}}">
<div class="caption">
<h3>{{result}}</h3>
<p>{{result}}</p>
<p><a href="{{result.videoUrl}}"
class="btn btn-default" role="button">Watch</a></p>
</div>
</div>
</div>
`
})
export class SearchResultComponent {
result: SearchResult;
}
#Component({
selector: 'weather-search',
template: `
<div class='container'>
<div class="page-header">
<h1>Weather Search</h1>
</div>
<div class="row">
<div class="input-group input-group-lg col-md-12">
<search-box
(loading)="loading = $event"
(results)="updateResults($event)"
></search-box>
</div>
</div>
<div class="row">
<search-result
*ngFor="let result of results"
[result]="result">
</search-result>
</div>
</div>
`
})
export class WeatherSearchComponent {
results: SearchResult[];
updateResults(results: SearchResult[]): void {
this.results = results;
// console.log("results:", this.results); // uncomment to take a look
}
}
This is what i do recive from api call :
/*
{"coord":{"lon":18.65,"lat":54.35},
"weather":[{"id":801,"main":"Clouds",
"description":"few clouds","icon":"02n"}],"base":"stations",
"main":{"temp":270.15,"pressure":1035,"humidity":92,
"temp_min":270.15,"temp_max":270.15},"visibility":10000,
"wind":{"speed":2.1,"deg":290},"clouds":{"all":20},
"dt":1484665200,
"sys":{"type":1,"id":5349,"message":0.0029,
"country":"PL","sunrise":1484636080,"sunset":1484665040},"
id":3099434,"name":"Gdansk","cod":200}
*/
It's not clear from your question whether you're trying to .map() on a variable or on an observable.
Scenario #1: variable.map()
variable must be an array. Looks from your code that your variable contains an object.
const jsonData = {
"coord":{"lon":18.65,"lat":54.35},
// Abridged for brevity...
id":3099434,"name":"Gdansk","cod":200
};
jsonData.map(...); // WON'T WORK - `jsonData` must be an array
Scenario #2: observable.map()
You must import the map operator in your code before mapping:
import 'rxjs/add/operator/map';
// Then, later
Observable.map(...);

Unable to test a component with a service

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.

How to stop redux-form or React from changing htmlFor and id when creating Jest snapshots?

I've got a wizard form made with redux-forms v6 and it looks something like:
--
index.js - Holds page number in local state, is connected to application level state
PageOne - wrapped with reduxForm decorator (form: 'wizForm')
PageTwo - wrapped with reduxForm decorator (form: 'wizForm')
--
PageOne and PageTwo both contain additional components that render sections of the form (initial fields, vehicle information, driver information...), and each of those sections render their own components for each question in that section.
Since there's a lot of nested components and I want to test that PageOne and PageTwo call the props passed from index.js, I've resorted to using Enzyme's mount() function with a fake store. I want to MatchSnapshot() with Jest to compare whether index.js is rendering PageOne or PageTwo, after certain buttons are clicked to go back and forth from pages.
The problem is when I do create snapshots, other than creating a 16,000 line snapshot, the snapshot will NEVER match the previous one even if I don't change anything. I'm not sure if it's redux-form that's doing it or React, but the htmlFor and the id keep changing between snapshots, test after test after test.
We use css-modules too, but I don't think that's causing the problem, and we did configure Jest to work with css-modules too, modifying "moduleNameWrapper" to mock .css files. Does anyone know how to fix this or where I should look?
tests:
describe('<VehicleAddition />', () => {
let props;
beforeEach(() => {
props = {
...,
};
});
it('Renders initially', () => {
const component = shallow(<VehicleAddition {...props} />);
expect(toJson(component)).toMatchSnapshot();
});
it('Renders <PageTwo> when <PageOne> form is submitted', () => {
const component = shallow(<VehicleAddition {...props} />);
expect(toJson(component)).toMatchSnapshot();
component.find('ReduxForm') // reduxForm HOC wraps the <form> in a <ReduxForm> component
.first()
.simulate('submit');
expect(toJson(component)).toMatchSnapshot();
expect(component.state().page).toEqual(2);
});
it('PageTwoStuffs', () => {
// Render the form, click 'next', assert it's page two
// click 'previous'
jest.enableAutomock();
const store = createStore(
combineReducers({
route: jest.fn(() => Immutable.fromJS({})),
language: jest.fn(() => Immutable.fromJS({})),
global: jest.fn(() => Immutable.fromJS({})),
form: formReducer,
}),
Immutable.fromJS({}),
);
const component = mount(
<Provider store={store}>
<VehicleAddition {...props} />
</Provider>
);
// CAN'T check the state of <VehicleAddition /> because it can only be done on root component, says the error message.
expect(toJson(component)).toMatchSnapshot();
index.js:
export class VehicleAddition extends React.Component { // eslint-disable-line
constructor(props) {
super(props);
this.state = {
page: 1,
};
}
nextPage = () => {
this.setState({ page: this.state.page + 1 });
}
previousPage = () => {
this.setState({ page: this.state.page - 1 });
}
render() {
return (
<div>
{page === 1 &&
<PageOne
{...this.props}
/>
}
{page === 2 &&
<PageTwo
{...this.props}
/>
}
</div>
);
}
}
PageOne.js
class PageOne extends React.Component { // eslint-disable-line
render() {
const {
...
} = this.props;
return (
<form onSubmit={handleSubmit}>
<div>
<InitialFields
autoPolicies={autoPolicies}
changeField={this.changeField}
getFormValues={getFormValues}
policies={policies}
primary={primary}
/>
<VehicleBeingAddedFields
changeField={this.changeField}
getFormValues={getFormValues}
fetchVehMakes={fetchVehMakes}
fetchVehModels={fetchVehModels}
policies={policies}
vehMakes={vehMakes}
vehModels={vehModels}
/>
...
<div className="btn-group btn-group-float-right">
<button
type="submit"
onClick={this.handleClick}
disabled={pristine || submitting}
className="btn-primary"
>
Next
</button>
</div>
</form>
);
}
}
PageTwo.js:
class PageTwo extends React.Component { // eslint-disable-line
render() {
const {
...
} = this.props;
return (
<form onSubmit={handleSubmit}>
...
<div className="btn-group btn-group-float-right">
<button type="button" className="btn" onClick={previousPage}>Previous</button>{' '}
<button type="submit" disabled={pristine || submitting} className="btn-primary">Submit</button>
</div>
</form>
);
}
}
Example of the parts of the snapshot that constantly changes:
I solved it by passing a hardcoded id value from the test cases
import React from 'react';
import renderer from 'react-test-renderer';
import { reduxForm } from 'redux-form';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { mount } from 'enzyme'
import TodoItem from './TodoItem';
import injectTapEventPlugin from 'react-tap-event-plugin';
function setup() {
const spy = jest.fn();
const store = createStore(() => ({}));
const Decorated = reduxForm({ form: 'testForm' })(TodoItem);
const props = {
remove: jest.fn(),
TodoItemReduxFormInitialName: "fullName",
snapshotTestId:"4"
}
const mockedComponent = <Provider store={store}>
<Decorated {...props} />
</Provider>;
const enzymeWrapper = mount(mockedComponent)
injectTapEventPlugin();
return {
props,
mockedComponent,
enzymeWrapper
}
}
describe('TodoItem Component', () => {
it('should render the snapshot', () => {
const {mockedComponent} = setup()
const tree = renderer.create(
mockedComponent
).toJSON();
expect(tree).toMatchSnapshot();
});
//not required as snapshot testing covers it
it('should render Number', () => {
const {enzymeWrapper} = setup()
const fieldProps = enzymeWrapper.find('Field').at(0).props();
expect(fieldProps.hintText).toEqual('Item Number');
expect(fieldProps.name).toEqual('fullName.itemNumber');
});
//not required as snapshot testing covers it
it('should render remove button', () => {
const {enzymeWrapper} = setup()
const button = enzymeWrapper.find('RaisedButton').at(0).props();
expect(button.label).toEqual("remove")
});
});

How to mock service?

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

How to change value of a select box in angular2 unit test?

I have an Angular2 component that contains a select box that looks like
<select [(ngModel)]="envFilter" class="form-control" name="envSelector" (ngModelChange)="onChangeFilter($event)">
<option *ngFor="let env of envs" [ngValue]="env">{{env}}</option>
</select>
I am trying to write a unit test for the ngModelChange event. This is my latest failing attempt
it("should filter and show correct items", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
el = fixture.debugElement.query(By.name("envSelector"));
fixture.detectChanges();
makeResponse([hist2, longhist]);
comp.envFilter = 'env3';
el.triggerEventHandler('change', {});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(comp.displayedHistory).toEqual(longhist);
});
});
The part I am having trouble with is that changing the value of the underlying model comp.envFilter = 'env3'; does not trigger the change method. I added el.triggerEventHandler('change', {}); but this throws Failed: Uncaught (in promise): ReferenceError: By is not defined. I cannot find any hints in the documentation... any ideas?
As far as the error. It seems like you just need to import By. This is not something that is global. It should be imported from the following module
import { By } from '#angular/platform-browser';
As far as the testing part, this is what I have been able to figure out. When you change a value in a the component, you need to trigger a change detection to update the view. You do this with fixture.detectChanges(). Once this is done, normally the view should be updated with the value.
From testing something similar to your example, it seems this is not the case though. It seems there is still some asynchronous task going on after the change detection. Say we have the following
const comp = fixture.componentInstance;
const select = fixture.debugElement.query(By.css('select'));
comp.selectedValue = 'a value';
fixture.DetectChanges();
expect(select.nativeElement.value).toEqual('1: a value');
This doesn't seem to work. It appears there is some async going on causing the value not to be set yet. So we need to wait for the async tasks by calling fixture.whenStable
comp.selectedValue = 'a value';
fixture.DetectChanges();
fixture.whenStable().then(() => {
expect(select.nativeElement.value).toEqual('1: a value');
});
The above would work. But now we need to trigger the change event as that doesn't happen automatically.
fixture.whenStable().then(() => {
expect(select.nativeElement.value).toEqual('1: a value');
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
fixture.whenStable().then(() => {
// component expectations here
});
});
Now we have another asynchronous task from the event. So we need to stabilize it again
Below is a complete test that I tested with. It's a refactor of the example from the source code integration tests. They used fakeAsync and tick which is similar to using async and whenStable. But with fakeAsync, you can't use templateUrl, so I though it would be best to refactor it to use async.
Also the source code tests does kind of a double one way testing, first testing model to view, then view to model. While it looks like your test was trying to do kind of a two-way test, from model around back to model. So I refactored it a bit to suite your example better.
import { Component } from '#angular/core';
import { TestBed, getTestBed, async } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { By } from '#angular/platform-browser';
import { dispatchEvent } from '#angular/platform-browser/testing/browser_util';
#Component({
selector: 'ng-model-select-form',
template: `
<select [(ngModel)]="selectedCity" (ngModelChange)="onSelected($event)">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
class NgModelSelectForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
onSelected(value) {
}
}
describe('component: NgModelSelectForm', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ NgModelSelectForm ]
});
});
it('should go from model to change event', async(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const comp = fixture.componentInstance;
spyOn(comp, 'onSelected');
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
comp.selectedCity = comp.cities[1];
fixture.detectChanges();
const select = fixture.debugElement.query(By.css('select'));
fixture.whenStable().then(() => {
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
console.log('after expect NYC');
});
});
}));
});
I found peeskillet's answer very useful but sadly it is a little out of date as the way to dispatch an Event has been changed. I also found there was an unnecessary call to whenStable(). So here is an updated test using peeskillet's setup:
it('should go from model to change event', async(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const comp = fixture.componentInstance;
spyOn(comp, 'onSelected');
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
comp.selectedCity = comp.cities[1];
fixture.detectChanges();
const select = fixture.debugElement.query(By.css('select'));
fixture.whenStable().then(() => {
select.nativeElement.dispatchEvent(new Event('change'));
fixture.detectChanges();
expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
console.log('after expect NYC');
});
}));
Look this example, from angular source (template_integration_spec.ts)
#Component({
selector: 'ng-model-select-form',
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
class NgModelSelectForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
}
it('with option values that are objects', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
comp.selectedCity = comp.cities[1];
fixture.detectChanges();
tick();
const select = fixture.debugElement.query(By.css('select'));
const nycOption = fixture.debugElement.queryAll(By.css('option'))[1];
// model -> view
expect(select.nativeElement.value).toEqual('1: Object');
expect(nycOption.nativeElement.selected).toBe(true);
select.nativeElement.value = '2: Object';
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
tick();
// view -> model
expect(comp.selectedCity['name']).toEqual('Buffalo');
}));
Same problem as raised by OP but slightly different code.
Works in Angular 7.
HTML:
<select id="dashboard-filter" class="form-control" name="dashboard-filter" [ngModel]="dashboardFilterValue" (ngModelChange)="onFilterChange($event)"
[disabled]="disabled">
<option *ngFor="let filter of dashboardFilters" [ngValue]="filter.value">{{ filter.name }}</option>
</select>
Unit test:
it('onFilterChange', () => {
// ensure dropdown is enabled
expect(component.disabled).toBe(false)
// spies
spyOn(component, 'onFilterChange').and.callThrough()
spyOn(component.filterChange, 'emit')
// initially the 3rd item in the dropdown is selected
const INITIAL_FILTER_INDEX = 2
// we want to select the 5th item in the dropdown
const FILTER_INDEX = 4
// the expected filter value is the value of the 5th dashboard filter (as used to populate the dropdown)
const EXPECTED_FILTER_VALUE = getDashboardFiltersData.dashboardFilters[FILTER_INDEX].value
// handle on the dropdown
const filterDropdown = fixture.debugElement.query(By.css('select')).nativeElement
// let bindings complete
fixture.whenStable().then(() => {
// ensure filterDropdown.value is stable
expect(filterDropdown.value).toContain(getDashboardFiltersData.dashboardFilters[INITIAL_FILTER_INDEX].value)
// update filterDropdown.value and dispatch change event
filterDropdown.value = filterDropdown.options[FILTER_INDEX].value
filterDropdown.dispatchEvent(new Event('change'))
// check component data
expect(component.dashboardFilterValue).toBe(EXPECTED_FILTER_VALUE)
expect(component.dashboardFilterChangeInProgress).toBe(false)
// check spies
expect(component.onFilterChange).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
expect(setDashboardFilterSpy).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
expect(component.filterChange.emit).toHaveBeenCalledWith(true)
})
})