I'm attempting to test the two-way binding feature in Angular 2. I've also read through a few other answers but I still can't get the test to pass.
When the input field is updated, I would like to run a test that ensure the searchQuery property on the AppComponent class is the same as the value of the input field.
As mentioned, I've read a few other answers and as I've gone along included additional pieces of code. So what is there currently might not all be needed?
Component
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
template: '<input type="text" name="input" [(ngModel)]="searchQuery" (change)="onChange()" id="search">',
styles: ['']
})
export class AppComponent {
public searchQuery: string;
onChange() {
console.log(this.searchQuery);
}
}
Unit test
import { ComponentFixture, TestBed, async, fakeAsync, tick, ComponentFixtureAutoDetect } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { AppComponent } from './app.component';
import { FormsModule } from '#angular/forms';
describe('AppComponent', () => {
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ],
providers: [],
imports: [ FormsModule ],
schemas: []
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
it('should create the app', fakeAsync(() => {
const de = fixture.debugElement.query(By.css("#search"));
const el = de.nativeElement;
el.value = "My string";
var event = new Event('input', {
'bubbles': true,
'cancelable': true
});
el.dispatchEvent(event);
tick();
fixture.detectChanges();
expect(comp.searchQuery).toEqual("My string");
}));
});
If there is a better approach, I am of course happy to get any feedback around this.
You have to run
fixture.detectChanges();
before dispatching event to ensure that your control has initialized and registered onChange event
setUpControl function
// view -> model
dir.valueAccessor.registerOnChange(function (newValue) {
dir.viewToModelUpdate(newValue);
control.markAsDirty();
control.setValue(newValue, { emitModelToViewChange: false });
});
Plunker Example
See also
Angular2 NgModel not getting value in Jasmine test
Related
I'm working on angular 2 project, using ngrx (for Redux logic) and rxjs (for observable).
Now I try to testing the project by running the auto-created .spec.ts files.
Some of the testings are fail, all of them are testing of components which use Store (from ngrx).
Here is one of them:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { MyComponent } from './overlay-menu-item.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MyComponent ],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
I look after the problem, the error was:
No provider to Store
So I add: (I use jasmine for testing)
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MyComponent ],
providers: [Store]
})
.compileComponents();
}));
Now I got other error:
No provider for StateObservable
So I add:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MyComponent ],
providers: [Store, StateObservable]
})
.compileComponents();
}));
But now I got new error that I don't know what do to with it:
Failed: Can't resolve all parameters for StateObservable: (?).
Error: Can't resolve all parameters for StateObservable: (?).
at syntaxError [mywebprojectPath]/node_modules/#angular/compiler/#angular/compiler.es5.js:1689:22)
What should I do?
You should create a MockStore.
In short:
import { Action } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { map } from 'rxjs/operator/map';
export class MockStore<T> extends BehaviorSubject<T> {
constructor(private _initialState: T) {
super(_initialState);
}
dispatch = (action: Action): void => {
}
select = <T, R>(pathOrMapFn: any, ...paths: string[]): Observable<R> => {
return map.call(this, pathOrMapFn);
}
}
Then you can provide the mockStore in you tests:
const initialState = {...};
TestBed.configureTestingModule({
...
providers:[
{provide:Store, useValue: new MockStore(initialState)}
]
...
})
I have header component definition as following:
import { Component, OnChanges, Input } from '#angular/core';
#Component({
selector: 'app-section-header',
template:`
<div class="pageTitle">
<h1>{{name}}</h1>
<a class="editBtn" [routerLink]="routerLink">edit</a>
</div>
<div class="progress"></div>
`,
styleUrls: ['./section-header.component.css']
})
export class SectionHeaderComponent implements OnChanges {
public routerLink: string[];
#Input() name: string;
ngOnChanges() {
this.routerLink = ['/section', this.name, 'edit'];
}
}
this component gets binding 'name' from its parent component, later it used to form a part of routeLink to 'edit' screen.
It is working well when running application.
For some reason, I cannot test the correct creation of this link:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { SectionHeaderComponent } from './section-header.component';
import { RouterTestingModule } from '#angular/router/testing';
import { Component, Input, Injectable, OnChanges , SimpleChanges, Output,SimpleChange, EventEmitter} from '#angular/core'
fdescribe('SectionHeaderComponent', () => {
let component: SectionHeaderComponent;
let fixture: ComponentFixture<SectionHeaderComponent>;
let element, de;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [SectionHeaderComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SectionHeaderComponent);
component = fixture.componentInstance;
element = fixture.nativeElement; // to access DOM element
de = fixture.debugElement;
});
it('should create link to edit view', () => {
component.name='sasha';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('h1').innerText).toBe('Sasha');
//for some reason this test failing with error expected '/'is not
// equal to 'section/sasha/edit'
expect(de.query(By.css('a')).nativeElement.getAttribute('href')).toBe ('/section/sasha/edit');
});
});
});
Where am I go wrong?
Thanks
You need to call fixture.detectChanges() after the call to ngOnChanges(). After making this change, it should work
Plunker
it('should create link to edit view', () => {
component.name = 'sasha';
component.ngOnChanges();
fixture.detectChanges()
expect(element.querySelector('h1').innerText).toBe('sasha');
expect(de.query(By.css('a')).nativeElement.getAttribute('href'))
.toBe('/section/sasha/edit');
});
I have a login component as follows:
import { Component, OnInit } from '#angular/core';
import { MdDialogRef } from '#angular/material';
import { AuthService } from '../../core/services/auth.service';
#Component({
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginDialogComponent implements OnInit {
model: {
email: string,
password: string
};
error;
constructor(
private authService: AuthService,
private dialogRef: MdDialogRef<LoginDialogComponent>
) { }
ngOnInit() {
this.model = {
email: '',
password: ''
};
}
signin() {
this.error = null;
this.authService.login(this.model.email, this.model.password).subscribe(data => {
this.dialogRef.close(data);
}, err => {
this.error = err.json();
});
}
}
And I have a test spec for this component as follows:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { MdDialogRef, OverlayRef } from '#angular/material';
import { AuthService } from '../../core/services/auth.service';
import { LoginDialogComponent } from './login.component';
describe('Component: Login', () => {
let component: LoginDialogComponent;
let fixture: ComponentFixture<LoginDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
LoginDialogComponent
],
imports: [],
providers: [
AuthService,
MdDialogRef,
OverlayRef
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
I've tried a million different things, and no matter what I do, I get the following error:
Can't resolve all parameters for MdDialogRef: (?)
Here's the code for MdDialogRef, which only has 1 parameter, OverlayRef. What am I missing?
import { OverlayRef } from '../core';
import { Observable } from 'rxjs/Observable';
/**
* Reference to a dialog opened via the MdDialog service.
*/
export declare class MdDialogRef<T> {
private _overlayRef;
/** The instance of component opened into the dialog. */
componentInstance: T;
/** Subject for notifying the user that the dialog has finished closing. */
private _afterClosed;
constructor(_overlayRef: OverlayRef);
/**
* Close the dialog.
* #param dialogResult Optional result to return to the dialog opener.
*/
close(dialogResult?: any): void;
/** Gets an observable that is notified when the dialog is finished closing. */
afterClosed(): Observable<any>;
}
EDIT: taking a clue from #Ryan's comment, I tried removing the MdDialogRef provider entirely and got the following error:
Can't resolve all parameters for OverlayRef: (?, ?, ?)
This leads me to believe that the problem is actually w/MdDialogRef trying to resolve OverlayRef, not w/MdDialogRef itself.
WORKING EXAMPLE The code below is the actual working code, per Yurzui's suggestion.
/* tslint:disable:no-unused-variable */
import { NgModule } from '#angular/core';
import { async, TestBed } from '#angular/core/testing';
import { CommonModule } from '#angular/common';
import { FormsModule } from '#angular/forms';
import { MaterialModule, MdDialogModule, MdToolbarModule, MdDialog, MdDialogRef } from '#angular/material';
import { CoreModule } from '../../core/core.module';
import { LoginDialogComponent } from './login.component';
#NgModule({
declarations: [
LoginDialogComponent
],
entryComponents: [
LoginDialogComponent
],
exports: [
LoginDialogComponent
],
imports: [
CommonModule,
CoreModule,
FormsModule,
MaterialModule.forRoot(),
MdDialogModule.forRoot(),
MdToolbarModule.forRoot()
]
})
class LoginDialogSpecModule { }
describe('Component: Login Dialog', () => {
let component: LoginDialogComponent;
let dialog: MdDialog;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
LoginDialogSpecModule
]
});
});
beforeEach(() => {
dialog = TestBed.get(MdDialog);
let dialogRef = dialog.open(LoginDialogComponent);
component = dialogRef.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
There is an issue ComponentFactoryResolver is not aware of components compiled via TestBed
According to this problem angular2 team offers workaround by creating real module with entryComponents property
https://github.com/angular/material2/blob/2.0.0-beta.1/src/lib/dialog/dialog.spec.ts#L387-L402
So your test could write like this:
import { MdDialog, MdDialogModule } from '#angular/material';
#NgModule({
declarations: [TestComponent],
entryComponents: [TestComponent],
exports: [TestComponent],
})
class TestModule { }
describe('Component: Login', () => {
let component: TestComponent;
let dialog: MdDialog;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestModule, MdDialogModule]
});
});
beforeEach(() => {
dialog = TestBed.get(MdDialog);
let dialogRef = dialog.open(TestComponent);
component = dialogRef.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Plunker Example
I was getting the same error when running my code normally, i.e. I did not write a test case.
I found that the line provides: [ MdDialogRef ]in my main component was giving this exact same error, and everything worked without it.
I'm creating a unit test for my Navbar Component and I'm getting an error:
Can't bind to 'routerLink' since it isn't a known property of 'a'
Navbar Component TS
import { Component } from '#angular/core';
import { Router } from '#angular/router';
import { NavActiveService } from '../../../services/navactive.service';
import { GlobalEventsManager } from '../../../services/GlobalEventsManager';
#Component({
moduleId: module.id,
selector: 'my-navbar',
templateUrl: 'navbar.component.html',
styleUrls:['navbar.component.css'],
providers: [NavActiveService]
})
export class NavComponent {
showNavBar: boolean = true;
constructor(private router: Router,
private navactiveservice:NavActiveService,
private globalEventsManager: GlobalEventsManager){
this.globalEventsManager.showNavBar.subscribe((mode:boolean)=>{
this.showNavBar = mode;
});
}
}
Navbar Component Spec
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { NavComponent } from './navbar.component';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { Router } from '#angular/router';
export function main() {
describe('Navbar component', () => {
let de: DebugElement;
let comp: NavComponent;
let fixture: ComponentFixture<NavComponent>;
let router: Router;
// preparing module for testing
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NavComponent],
}).compileComponents().then(() => {
fixture = TestBed.createComponent(NavComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('p'));
});
}));
it('should create component', () => expect(comp).toBeDefined());
/* it('should have expected <p> text', () => {
fixture.detectChanges();
const h1 = de.nativeElement;
expect(h1.innerText).toMatch(" ");
});*/
});
}
I realize that I need to add router as a spy, but if I add it as a SpyObj and declare it as a provider I get the same error.
Is there a better way for me to add fix this error?
EDIT: Working Unit Test
Built this unit test based on the answer:
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { NavComponent } from './navbar.component';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { RouterLinkStubDirective, RouterOutletStubComponent } from '../../../../test/router-stubs';
import { Router } from '#angular/router';
import { GlobalEventsManager } from '../../../services/GlobalEventsManager';
import { RouterModule } from '#angular/router';
import { SharedModule } from '../shared.module';
export function main() {
let comp: NavComponent;
let fixture: ComponentFixture<NavComponent>;
let mockRouter:any;
class MockRouter {
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
}
describe('Navbar Componenet', () => {
beforeEach( async(() => {
mockRouter = new MockRouter();
TestBed.configureTestingModule({
imports: [ SharedModule ]
})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(SharedModule, {
remove: {
imports: [ RouterModule ],
},
add: {
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ],
providers: [ { provide: Router, useValue: mockRouter }, GlobalEventsManager ],
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(NavComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
function tests() {
let links: RouterLinkStubDirective[];
let linkDes: DebugElement[];
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
});
it('can instantiate it', () => {
expect(comp).not.toBeNull();
});
it('can get RouterLinks from template', () => {
expect(links.length).toBe(5, 'should have 5 links');
expect(links[0].linkParams).toBe( '/', '1st link should go to Home');
expect(links[1].linkParams).toBe('/', '2nd link should go to Home');
expect(links[2].linkParams).toBe('/upload', '3rd link should go to Upload');
expect(links[3].linkParams).toBe('/about', '4th link should to to About');
expect(links[4].linkParams).toBe('/login', '5th link should go to Logout');
});
it('can click Home link in template', () => {
const uploadLinkDe = linkDes[1];
const uploadLink = links[1];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/');
});
it('can click upload link in template', () => {
const uploadLinkDe = linkDes[2];
const uploadLink = links[2];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/upload');
});
it('can click about link in template', () => {
const uploadLinkDe = linkDes[3];
const uploadLink = links[3];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/about');
});
it('can click logout link in template', () => {
const uploadLinkDe = linkDes[4];
const uploadLink = links[4];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/login');
});
}
}
Just import RouterTestingModule in TestBed.configureTestingModule of your components spec.ts file
Eg:
import { RouterTestingModule } from '#angular/router/testing';
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ ComponentHeaderComponent ]
})
The Angular Testing docs address this by using RouterLinkDirectiveStub and RouterOutletStubComponent so that routerLink is a known property of <a>.
Basically it says that using RouterOutletStubComponent is a safe way to test routerLinks without all the complications and errors of using the real RouterOutlet. Your project needs to know it exists so it doesn't throw errors but it doesn't need to actually do anything in this case.
The RouterLinkDirectiveStub enables you to click on <a> links with routerLink directive and get just enough information to test that it is being clicked (navigatedTo) and going to the correct route (linkParams). Any more functionality than that and you really aren't testing your component in isolation any more.
Take a look at their Tests Demo in app/app.component.spec.ts. Grab the testing/router-link-directive-stub.ts and add to your project. Then you will inject the 2 stubbed items into your TestBed declarations.
If you want only isolated test and DO NOT CARE about template,you can add NO_ERRORS_SCHEMA. This tells Angular not to show error if it encounters any unknown attribute or element in HTML
Eg:
TestBed.configureTestingModule({
declarations: [ ComponentHeaderComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
})
I wrote a very simple custom validator for an input field:
import { Directive } from '#angular/core';
import { AbstractControl, NG_VALIDATORS } from '#angular/forms';
function numberValidator(c: AbstractControl) {
if (!c.value) return null;
return new RegExp('^[1-9][0-9]{6,9}$').test(c.value) ? null : {
validateNumber: {
valid: false
}
}
}
#Directive({
selector: '[number-validator]',
providers: [
{ provide: NG_VALIDATORS, multi: true, useValue: numberValidator }
]
})
export class NumberValidator {}
I would like to unit test this validator. I read Test an attribute directive on the Angular2 page, but there is no css or html that changes. How can I unit test this validator?
If you want to do it the easy way (which I would do, since all the logic is in the validator function), is just to test the validator function. Just pass a control to it
expect(numberValidator(new FormControl('123456'))).toEqual({
'validateNumber': { 'valid': false }
});
expect(numberValidator(new FormControl('123456789'))).toEqual(null);
If you really want to test it when "being used", then it gets a little tedious. These are usually the steps I take
Create dummy component to use the directive
Set up the test bed configuration
Create the component to test.
Get the native input element and dispatch an invalid input event to it
Get the injector that holds the NgForm
Check the form for failure
Put a valid input and check that it passes.
It's a lot compared to just testing the validator method. But here it is anyway ;-) Enjoy!
import { Component, Directive } from '#angular/core';
import { TestBed, async } from '#angular/core/testing';
import { dispatchEvent } from '#angular/platform-browser/testing/browser_util';
import { By } from '#angular/platform-browser';
import { FormsModule, NG_VALIDATORS, AbstractControl,
NgForm, FormControl } from '#angular/forms';
function numberValidator(c: AbstractControl) {
if (!c.value) return null;
return new RegExp('^[1-9][0-9]{6,9}$').test(c.value) ? null : {
validateNumber: {
valid: false
}
};
}
#Directive({
selector: '[number-validator]',
providers: [
{ provide: NG_VALIDATORS, multi: true, useValue: numberValidator }
]
})
export class NumberValidator {
}
#Component({
template: `
<form>
<input name="number" type="text" ngModel number-validator />
</form>
`
})
class TestComponent {
}
describe('component: TestComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [TestComponent, NumberValidator]
});
});
it('should validate (easy)', () => {
expect(numberValidator(new FormControl('123'))).toEqual({
'validateNumber': { 'valid': false }
});
expect(numberValidator(new FormControl('123456789'))).toEqual(null);
});
it('should validate (tedious)', async(() => {
let fixture = TestBed.createComponent(TestComponent);
let comp = fixture.componentInstance;
let debug = fixture.debugElement;
let input = debug.query(By.css('[name=number]'));
fixture.detectChanges();
fixture.whenStable().then(() => {
input.nativeElement.value = '123';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
let form: NgForm = debug.children[0].injector.get(NgForm);
let control = form.control.get('number');
// just to show a few different ways we can check validity
expect(control.hasError('validateNumber')).toBe(true);
expect(control.valid).toBe(false);
expect(form.control.valid).toEqual(false);
expect(form.control.hasError('validateNumber', ['number'])).toEqual(true);
input.nativeElement.value = '123456789';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.control.valid).toEqual(true);
});
}));
});