Angular 2 Unit Testing - Cannot read property 'root' of undefined - unit-testing

Error Description
Angular version: 2.3.1
My unit test fails to create the component - I know this issue is related to the [routerLink] and [routerLinkActive] directives because removing them from the template allows the test to create the component.
TEMPLATE
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button class="navbar-toggle" data-toggle="collapse" data-target="#iotahoe-top-navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" [routerLink]="['/']">IoTahoe</a>
</div>
<div class="collapse navbar-collapse" id="iotahoe-top-navigation">
<ul *ngIf="isAuthenticated()" class="nav navbar-nav navbar-right">
<li [routerLinkActive]="['active']"><a [routerLink]="['/dashboard']">Dashboard</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/browse']">Browse</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/admin']">Admin</a></li>
<li [routerLinkActive]="['active']"><a (click)="onLogout()" style="cursor: pointer;">Logout</a></li>
</ul>
</div>
TYPESCRIPT
import { Component, OnInit } from '#angular/core';
import { AuthenticationService } from '../../authentication/authentication.service';
import { Router } from '#angular/router';
#Component({
moduleId: module.id.toString(),
selector: 'app-top-navbar',
templateUrl: './top-navbar.component.html',
styleUrls: ['./top-navbar.component.css']
})
export class TopNavbarComponent implements OnInit {
constructor(private authenticationService: AuthenticationService, private router: Router) { }
ngOnInit() {
}
isAuthenticated() {
return this.authenticationService.isLoggedIn;
}
onLogout() {
this.authenticationService.logout().subscribe(() => {
return this.router.navigate(['/login']);
});
}
}
TEST SPEC
/* tslint:disable:no-unused-variable */
import {async, ComponentFixture, TestBed, inject} from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import {DebugElement, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, Component} from '#angular/core';
import { RouterTestingModule } from '#angular/router/testing';
import { Location, CommonModule } from '#angular/common';
import { TopNavbarComponent } from './top-navbar.component';
import { AuthenticationService } from '../../authentication/authentication.service';
import { Router } from '#angular/router';
import {ReactiveFormsModule} from "#angular/forms";
#Component({
template: ''
})
class DummyComponent {
}
describe('TopNavbarComponent', () => {
let component: TopNavbarComponent;
let fixture: ComponentFixture<TopNavbarComponent>;
let authenticationService: AuthenticationService;
beforeEach(async(() => {
const authenticationServiceStub = {
isLoggedIn: false
};
const routerStub = {
navigate: jasmine.createSpy('navigate'),
navigateByUrl: jasmine.createSpy('navigateByUrl')
};
TestBed.configureTestingModule({
declarations: [ TopNavbarComponent, DummyComponent ],
imports:[CommonModule, ReactiveFormsModule, RouterTestingModule.withRoutes(
[
{ path: '/', component:DummyComponent },
{ path: '/login', component:DummyComponent },
{ path: '/dashboard', component:DummyComponent },
{ path: '/browse', component:DummyComponent },
{ path: '/admin', component:DummyComponent }
])],
providers: [
{ provide: AuthenticationService, useValue: authenticationServiceStub },
{ provide: Router, useValue: routerStub }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopNavbarComponent);
component = fixture.componentInstance;
authenticationService = TestBed.get(AuthenticationService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
ERROR
zone.js:155 Uncaught Error: Error in package:407:9:6 caused by: Cannot
read property 'root' of undefined
at ViewWrappedError.Error (native)
at ViewWrappedError.ZoneAwareError (localhost:9876/base/src/test.ts:133296:33)
at ViewWrappedError.BaseError [as constructor] (localhost:9876/base/src/test.ts:35630:16)
at ViewWrappedError.WrappedError [as constructor] (localhost:9876/base/src/test.ts:35695:16)
at new ViewWrappedError (localhost:9876/base/src/test.ts:68018:16)
at DebugAppView._rethrowWithContext (localhost:9876/base/src/test.ts:108242:23)
at DebugAppView.create (localhost:9876/base/src/test.ts:108142:18)
at DebugAppView.View_TopNavbarComponent_Host0.createInternal (/DynamicTestModule/TopNavbarComponent/host.ngfactory.js:16:19)
at DebugAppView.AppView.createHostView (localhost:9876/base/src/test.ts:107700:21)
at DebugAppView.createHostView (localhost:9876/base/src/test.ts:108156:52)
at ComponentFactory.create (localhost:9876/base/src/test.ts:49830:25)
at initComponent (localhost:9876/base/src/test.ts:6425:53)
at ZoneDelegate.invoke (localhost:9876/base/src/test.ts:132727:26)
at ProxyZoneSpec.onInvoke (localhost:9876/base/src/test.ts:95802:39)
at ZoneDelegate.invoke (localhost:9876/base/src/test.ts:132726:32)Zone.runTask #
zone.js:155ZoneTask.invoke # zone.js:345data.args.(anonymous function)
# zone.js:1376

The routerLink directives need a real router, but you are mocking it. A couple things I can see you doing:
If you don't plan to click the links during testing, then you can mock those directive to just make "noop" directives so the compiler doesn't complain. e.g.
#Directive({
selector: '[routerLink], [routerLinkActive]'
})
class DummyRouterLinkDirective {}
Then just add that to the declarations of the test module. With this you don't really need to configure the RouterTestingModule at all. You could probably get rid of that.
Also if you don't plan to click test, another option (without needing to create dummy directives is to just ignore the errors of missing directives:
schemas: [ NO_ERRORS_SCHEMA ]
You would add this to the test module configuration (as seen here). This might not be desirable in some cases, as it could also ignore errors that you actually want detected, which could lead to hard to debug tests.
If you would actually like to click the links and test routing, then you can use the real router. You can see an example of how you can test the navigation, using the Location, as seen in this post.

For me the solution with the non-mocked routing worked. But I found out that I also needed to add a
<router-outlet></router-outlet>
to the component using "routerlink active".

Related

Unexpected value 'SohoComponentsModule' imported by the module 'DynamicTestModule'

I am writing an angular 2 application and am using our UI team's library. The development of the application is going great, I can use all of their components easily by using an import statement like so. This is my app.module.ts:
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { SohoComponentsModule } from '#infor/sohoxi-angular'; // This is the one in question.
import { AppComponent } from './app.component';
import { licenseGeneratorComponent } from './licenseGenerator.component';
import { emergencyLicenseComponent } from './tabs/emergency/emergencyLicense.component';
import { partnerLicenseComponent } from './tabs/partner/partnerLicense.component';
import { standardLicenseComponent } from './tabs/standard/standardLicense.component';
#NgModule({
declarations: [
AppComponent,
licenseGeneratorComponent,
emergencyLicenseComponent,
partnerLicenseComponent,
standardLicenseComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
SohoComponentsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
and here is one of my templates used within this app, the soho stuff is from this library:
<div class="row">
<div class="twelve columns">
<h1>License Generator</h1>
<div soho-tabs>
<ul soho-tab-list>
<li soho-tab><a soho-tab-title tabId='tabs-normal-emergency'>Emergency Licenses</a></li>
<li soho-tab><a soho-tab-title tabId='tabs-normal-partner'>Partner Licenses</a></li>
<li soho-tab><a soho-tab-title tabId='tabs-normal-standard'>Standard Licenses</a></li>
</ul>
<div soho-tab-panel tabId='tabs-normal-emergency'>
<emergency-license></emergency-license>
</div>
<div soho-tab-panel tabId='tabs-normal-partner'>
<partner-license></partner-license>
</div>
<div soho-tab-panel tabId='tabs-normal-standard'>
<standard-license></standard-license>
</div>
</div>
</div>
</div>
The application has been working just fine, no issues. But when there is an issue with my unit tests now. I had them working before with the first few unit tests but left it alone for a week or so and added more content and now the tests don't run properly.
When I run my tests I get the error
Unexpected value 'SohoComponentsModule' imported by the module 'DynamicTestModule'
My app.component.spec looks like this:
import { TestBed, async } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { AppComponent } from './app.component';
import { licenseGeneratorComponent } from './licenseGenerator.component';
import { emergencyLicenseComponent } from './tabs/emergency/emergencyLicense.component';
import { partnerLicenseComponent } from './tabs/partner/partnerLicense.component';
import { standardLicenseComponent } from './tabs/standard/standardLicense.component';
import { SohoComponentsModule } from '#infor/sohoxi-angular';
describe('AppComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
licenseGeneratorComponent,
emergencyLicenseComponent,
partnerLicenseComponent,
standardLicenseComponent,
],
imports: [
FormsModule,
SohoComponentsModule
],
});
});
it('should create the app', async(() => {
let fixture = TestBed.createComponent(AppComponent);
let app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app works!'`, async(() => {
let fixture = TestBed.createComponent(AppComponent);
let app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app works!');
}));
});
I am going in circles now though because if I remove the import statement in the test it throws these errors:
Can't bind to 'closeOnSelect' since it isn't a known property of 'select'. ("label required">Versions</label>
Please Help!
Keep in mind the application works just fine still, just the unit tests that are giving me issues. I've spent all day looking around for fixes but can't figure this out.
Please let me know if I need to post more of my files.
Thanks,
Chris
For some reason it was the angular library that was causing this issue.
I updated the version and the issue was fixed.

Angular2 testing error when using templateUrl

I am writing some tests for my Angular2 application according to the documentation but I have run into a problem which I can't seem to fix. I get the following error when trying to launch the spec runner:
Failed: This test module uses the component CategoriesComponent which is using a "templateUrl", but they were never compiled. Please call "TestBed.compileComponents" before your test.
I understand this is happening as I am using a seperate template file for the template within the component but I jhave tried multilpe solutions which don't seem to work.
Here is my component under test:
import { Component } from "#angular/core";
#Component({
selector: 'categories-component',
templateUrl: '/app/views/catalog/categories/categories-dashboard.html',
moduleId: module.id
})
export class CategoriesComponent {
title: 'Categories;
}
The categories-dashboard.html file:
<h1>{{ title }}</h1>
and my testing module for the component:
import {TestBed, ComponentFixture, ComponentFixtureAutoDetect, async} from "#angular/core/testing";
import { By} from "#angular/platform-browser";
import { CategoriesComponent } from "../../../../components/catalog/categories/CategoriesComponent";
import { DebugElement } from "#angular/core";
let comp: CategoriesComponent;
let fixture: ComponentFixture<CategoriesComponent>;
let de: DebugElement;
let el: HTMLElement;
describe('BannerComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CategoriesComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
});
TestBed.compileComponents();
fixture = TestBed.createComponent(CategoriesComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
}));
it('should display original title', () => {
expect(el.textContent).toContain(comp.title);
});
});
I have tried to implement TestBed.compileComponents() into into the component but wherever I put it it doesn't seem to work.
Can anyone see why this error is occurring or point me in the directoin of a solution?
Thanks!
compileComponents resolves asynchronously (as it makes an XHR for the template), so it returns a promise. You should handle anything requiring the resolution of the promise, inside of the then callback of the promise
TestBed.compileComponents().then(() =>{
fixture = TestBed.createComponent(CategoriesComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});

Angular2 "Final" Unit Test Override Directive

I am using Angular 2 Final, and using the TestBed for Unit tests
I have an issue when when using "overrideDirective" it seems that the actual directive is being called, as I am getting an error "No provider for Config!" which MyDirective that was replaced is dependent on.
Component View
<div>
<input myDirective>
// some more logic etc...
</div>
Directive to Replace
#Directive({
selector: "myDirective"
})
class MyDirective{
constructor(
private config: Config
) {
}
}
Spec File
#Component({
template: "<my-component></my-component>"
})
class TestComponent{
}
#Directive({
selector: "fake"
})
class FakeDirective {
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
MyComponent,
MyDirective,
TestComponent,
FakeDirective
]
});
});
beforeEach(async(() => {
TestBed
.overrideDirective(MyDirective, FakeDirective)
.compileComponents();
fixture = TestBed.createComponent(TestComponent);
}));
Any one encountered the issue and managed to fix it please?
Thanks

Angular2 How to POST data using a service class

I have a simple application form which I am trying to post to the server. I am fairly new to Angular2
How can I pass the data from the component to the service and onto the server for a POST request.
The POST is working fine when I try it directly from FireFox plugin 'httpRequester'
This is the TaskComponent.ts
#Component({
selector: 'tasks',
template: `<div mdl class="mdl-grid demo-content">
<div class="demo-graphs mdl-shadow--2dp mdl-color--white mdl-cell mdl-cell--8-col">
<h3>Create Task Page</h3>
<form action="#" (ngSubmit)="onSubmit()">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input class="mdl-textfield__input" type="text" pattern="[A-Z,a-z]*" id="taskname" [(ngModel)]="data.taskname"/>
<label class="mdl-textfield__label" for="taskname">Task Name</label>
<span class="mdl-textfield__error">Only alphabet and no spaces, please!</span>
</div>
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored" type="submit">Create Task</button>
</form>
`,
directives: [ROUTER_DIRECTIVES, MDL]
})
export class CreateTaskComponent {
data: any
constructor() {
this.data = {
//taskname: 'Example Task'
};
}
onSubmit(form) {
console.log(this.data.taskname); <--Data is passed upon submit onto the console. Works fine.
//Need to call the postApartment method of ApartmentService
}
}
ApartmentService.ts
import {Http, Response} from 'angular2/http'
import {Injectable} from 'angular2/core'
import 'rxjs/add/operator/map';
#Injectable()
export class ApartmentService {
http: Http;
constructor(http: Http) {
this.http = http;
}
getEntries() {
return this.http.get('./api/apartments').map((res: Response) => res.json());
}
getProfile(userEmail :string){
return this.http.get(`./api/apartments/getprofile/${userEmail}`).map((res: Response) => res.json());
}
postApartment(){
// Not familiar with the syntax here
}
}
Server.ts
router.route('/api/apartments')
.post(function(req, res) {
var apartment = new Apartment();
apartment.name = req.body.name;
apartment.save(function(err) {
if (err)
res.send(err);
res.json({ message: 'Apartment created!' });
});
})
You can inject service via dependency injection and use it in the component
export class CreateTaskComponent {
constructor(){private _apartmentService: ApartmentService}{}
}
And you can access this in any of the component function via
onSubmit(form) {
console.log(this.data.taskname); <--Data is passed upon submit onto the console. Works fine.
//Need to call the postApartment method of ApartmentService
this._apartmentService.postApartment()
}
And when bootstraping the component you have to add it as dependency via
bootstrap(AppComponent, [ApartmentService]);
Another option for doing the last step is by added providers in the Component decorator like
#Component{
providers: [ApartmentService]
}
Inject the apartmentService in the component, No need of providers as I have bootstrapped it. (If you bootstartp the service, Do not include it in providers. It breaks the system)
export class CreateTaskComponent {
data: any
constructor(private apartmentService: ApartmentService) {
this.data = {};
}
onSubmit(form) {
this.apartmentService.postApartment(this.data);
}
}
The critical piece is the postApartment() method in the service
postApartment(data :any){
return this.http.post('/api/apartments',
JSON.stringify(data),{headers : new Headers({'Content-Type':'application/json'})
})
.map((res: Response) => res.json()).subscribe();
}
Also make sure on the server.js code, the mongoose fields match the http body parameters being passed.
I had to fix it to make it work.

Angular 2 http post reloads page, logout doesn't delete session cookie

I have an Angular 2 service that has a logout function. When the function is called from the app component it cause a full page refresh. When using angular 1 projects I haven't experienced this behavior. If I call my logout endpoint with postman the session cookie is deleted. It is not deleted if I use my angular 2 authentication service.
Service
import {Injectable} from 'angular2/core';
import {User} from './user';
import {Headers, RequestOptions, Http, Response} from 'angular2/http';
import {Observable} from 'rxjs/Observable';
import {Cookie} from '../extensions/cookies';
#Injectable()
export class AuthenticationService {
constructor (private http: Http) {}
private _prepTestHost = 'http://localhost:8000/';
private _prepTestLoginUrl = this._prepTestHost + 'login/';
private _prepTestLogoutUrl = this._prepTestHost + 'logout/';
private _authenticated: boolean;
getUser() {}
isAuthenticated() {
return this._authenticated;
}
setAuthenticated() {
this._authenticated = true;
}
loginUser(username, password) : Observable<User> {
let body = JSON.stringify({username, password});
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._prepTestLoginUrl, body, options)
.map(res => <User> res.json(), this.setAuthenticated())
.catch(this.handleError)
}
logoutUser() : Observable<void> {
let body = JSON.stringify({});
let csrfcookie = Cookie.getCookie('csrftoken');
let headers = new Headers({
'X-CSRFToken': csrfcookie,
'Content-Type': 'application/json'
});
let options = new RequestOptions({ headers: headers});
return this.http.post(this._prepTestLogoutUrl, body, options)
.map(res => <void> res.json())
.catch(this.handleError);
}
private handleError (error: Response) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
App Component
import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {WelcomeCenterComponent} from './welcome-center/welcome-center.component';
import {AuthenticationService} from './authentication/authentication.service';
import {LoginModalComponent} from './authentication/login-modal.component';
import {BrowserXhr, HTTP_PROVIDERS} from "angular2/http";
import {CORSBrowserXHR} from './extensions/corsbrowserxhr';
import {provide} from "angular2/core";
#Component({
selector: 'my-app',
template: `
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#" [routerLink]="['WelcomeCenter']">Brand</a>
</div>
<ul class="nav navbar-nav navbar-right">
<li *ngIf="!authenticated()">
Login
</li>
<li *ngIf="authenticated()">
Logout
</li>
</ul>
</div>
</nav>
<router-outlet></router-outlet>
<login-modal></login-modal>
`,
directives: [ROUTER_DIRECTIVES, LoginModalComponent],
providers: [HTTP_PROVIDERS,
provide(BrowserXhr, {useClass: CORSBrowserXHR}),
AuthenticationService]
})
#RouteConfig([
{
path: '/welcome-center/...',
name: 'WelcomeCenter',
component: WelcomeCenterComponent,
useAsDefault: true
}
])
export class AppComponent {
constructor(private _authenticationService: AuthenticationService) {}
authenticated() {
return this._authenticationService.isAuthenticated();
}
logout() {
console.log("Logout button pressed");
this._authenticationService.logoutUser().subscribe();
}
}
Setting withCredentials attribute:
import {BrowserXhr, HTTP_PROVIDERS} from "angular2/http";
import {Injectable, provide} from "angular2/core";
#Injectable()
export class CORSBrowserXHR extends BrowserXhr{
build(): any{
var xhr:any = super.build();
xhr.withCredentials = true;
return xhr;
}
}
I think that the page reload is due to the fact that you don't prevent event propagation when blocking on the layout button (you an 'a' HTML element with an 'href' attribute). You could use 'return false' at the end of your logout function or '$event.stopPropagation()'.
See the question for more details:
Stop event propagation in Angular 2
Regarding the cookie problem, I these that you use cross domain requests (CORS). I think that you should try to set to true the 'withCredentials' attribute on the underlying XHR object. See this question for more details:
Set-cookie in response not set for Angular2 post request
You could do something kind of hacky but I can't think of another way.
Cookie.setCookie(nameOfCookie, "", -1);
This would effectively delete the cookie on logout. I'd love to know if there was a better way though!
I also am not sure why you are getting any kind of page reload at all, I have yet to experience that on anything I've done yet, hopefully someone else will know.