I have a problem with defining asynchrous validator in template driven form.
Currently i have this input:
<input type="text" ngControl="email" [(ngModel)]="model.applicant.contact.email" #email="ngForm" required asyncEmailValidator>
with validator selector asyncEmailValidator which is pointing to this class:
import {provide} from "angular2/core";
import {Directive} from "angular2/core";
import {NG_VALIDATORS} from "angular2/common";
import {Validator} from "angular2/common";
import {Control} from "angular2/common";
import {AccountService} from "../services/account.service";
#Directive({
selector: '[asyncEmailValidator]',
providers: [provide(NG_VALIDATORS, {useExisting: EmailValidator, multi: true}), AccountService]
})
export class EmailValidator implements Validator {
//https://angular.io/docs/ts/latest/api/common/Validator-interface.html
constructor(private accountService:AccountService) {
}
validate(c:Control):{[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 (!EMAIL_REGEXP.test(c.value)) {
return {validateEmail: {valid: false}};
}
return null;
/*return new Promise(resolve =>
this.accountService.getUserNames(c.value).subscribe(res => {
if (res == true) {
resolve(null);
}
else {
resolve({validateEmailTaken: {valid: false}});
}
}));*/
}
}
Email regex part is working as expected and form is being validated successfuly if regex is matching. But after that I want to check if e-mail is not already in use, so im creating promise for my accountService. But this doesn't work at all and form is in failed state all the time.
I've read about model driven forms and using FormBuilder as below:
constructor(builder: FormBuilder) {
this.email = new Control('',
Validators.compose([Validators.required, CustomValidators.emailFormat]), CustomValidators.duplicated
);
}
Which have async validators defined in third parameter of Control() But this is not my case because im using diffrent approach.
So, my question is: is it possible to create async validator using template driven forms?
You could try to register the provider of your async validator with the NG_ASYNC_VALIDATORS key and not the NG_VALIDATORS one (only for synchronous validators):
#Directive({
selector: '[asyncEmailValidator]',
providers: [
provide(NG_ASYNC_VALIDATORS, { // <------------
useExisting: EmailValidator, multi: true
}),
AccountService
]
})
export class EmailValidator implements Validator {
constructor(private accountService:AccountService) {
}
validate(c:Control) {
return new Promise(resolve =>
this.accountService.getUserNames(c.value).subscribe(res => {
if (res == true) {
resolve(null);
}
else {
resolve({validateEmailTaken: {valid: false}});
}
}));
}
}
See this doc on the angular.io website:
https://angular.io/docs/ts/latest/api/forms/index/NG_ASYNC_VALIDATORS-let.html
worth noting that the syntax has changed since then, now i am using angular 4, and here below a rewrite:
import { Directive, forwardRef } from '#angular/core';
import { AbstractControl, Validator, NG_ASYNC_VALIDATORS } from '#angular/forms';
import { AccountService } from 'account.service';
#Directive({
selector: '[asyncEmailValidator]',
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => EmailValidatorDirective), multi: true
},
]
})
export class EmailValidatorDirective implements Validator {
constructor(private _accountService: AccountService) {
}
validate(c: AbstractControl) {
return new Promise(resolve =>
this._accountService.isEmailExists(c.value).subscribe(res => {
if (res == true) {
resolve({ validateEmailTaken: { valid: false } });
}
else {
resolve(null);
}
}));
}
}
I am able to correctly call validate custom validators using user service. One problem i was getting was that, I kept my custom validator inside Validators.compose(). After taking out of the compose function everything works.
import { Directive } from '#angular/core';
import { AsyncValidator, AbstractControl, ValidationErrors, NG_ASYNC_VALIDATORS, AsyncValidatorFn } from '#angular/forms';
import { Observable } from 'rxjs';
import { UserService } from '../Services/user.service';
import { map } from 'rxjs/operators';
export function UniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
const q = new Promise((resolve, reject) => {
setTimeout(() => {
userService.isUsernameTaken(control.value).subscribe((data: any) => {
// console.log('top: ' + data + ' type: ' + typeof data);
if (data === false) {
resolve(null);
} else {
resolve({
usernameTaken: {
valid: true
}
});
}
}, () => {
resolve({
usernameTaken: {
valid: false
}
});
});
}, 1000);
});
return q;
};
}
#Directive({
selector: '[appUniqueUsername]',
providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: UniqueUsernameValidatorDirective, multi: true }, UserService]
})
export class UniqueUsernameValidatorDirective implements AsyncValidator {
constructor(private userService: UserService) { }
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return UniqueUsernameValidator(this.userService)(control);
}
}
Related
I have searched and cannot find an answer to my issue. I have an Angular 5 project and I'm trying to run my unit tests and I'm getting the error:
TypeError: Cannot set property 'search' of undefined
Here is my ts file:
import { Component, OnChanges, OnInit, Input } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { CheckboxModule } from '#common-web-components';
import { ItemFilter } from '../../models/item-filter';
import { ItemBrandSearchResponse } from '../../models/item-brand-search-response';
import { ItemBrandSearchResponseService } from '../../services/item-brand-search-response.service';
#Component({
selector: 'app-item-brand-filter',
templateUrl: './item-brand-filter.component.html',
styleUrls: ['./item-brand-filter.component.css']
})
export class ItemBrandFilterComponent implements OnInit, OnChanges {
#Input()
filter: ItemFilter;
availableCount: number;
firstOpen: Boolean = true;
searching: Boolean = true;
itemBrands = new Array<ItemBrandSearchResponse>();
constructor(private itemBrandSearchResponseService: ItemBrandSearchResponseService) { }
ngOnInit() {
this.searching = true;
this.firstOpen = false;
this.getItemBrandForFilter();
}
ngOnChanges() {
if (!this.firstOpen) {
this.searching = true;
this.getItemBrandForFilter();
}
}
getItemBrandForFilter(): void {
this.itemBrandSearchResponseService.get(this.filter).subscribe(
results => {
this.itemBrands = results.data;
if (results.availableCount) {
this.availableCount = results.availableCount;
}
this.searching = false;
},
error => {
console.error('Error getting items');
}
);
}
getRouterLink(): string {
return this.filter.search === '' ? '/items' : '/items/search/' + this.filter.search;
}
}
And here is my spec file:
import { FormsModule } from '#angular/forms';
import { async, ComponentFixture, TestBed, inject, fakeAsync, tick } from '#angular/core/testing';
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { RouterTestingModule } from '#angular/router/testing';
import { CheckboxModule } from '#kehe-dev/connect-common-web-components';
import { ItemBrandFilterComponent } from './item-brand-filter.component';
import { ItemBrandSearchResponseService } from '../../services/item-brand-search-response.service';
import { ItemFilter } from '../../models/item-filter';
describe('Component: ItemBrandFilterComponent', () => {
let component: ItemBrandFilterComponent;
let fixture: ComponentFixture<ItemBrandFilterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ItemBrandFilterComponent
],
imports: [
CheckboxModule,
FormsModule,
HttpClientTestingModule,
RouterTestingModule
],
providers: [
ItemBrandSearchResponseService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemBrandFilterComponent);
component = fixture.componentInstance;
component.filter.search = 'test';
fixture.detectChanges();
});
it('should create', inject([ItemBrandSearchResponseService], (service: ItemBrandSearchResponseService) => {
expect(component).toBeTruthy();
}));
});
I originally didn't have the line:
component.filter.search = 'test';
And I had the error:
TypeError: Cannot read property 'search' of null
So I searched for that one and found an article that said I needed to set it. Now I get this new error and really just can't find anything that fits as a solution.
I found my solution. Within the test environment my component.filter hadn't been initialized.
So I added:
component.filter = new ItemFilter();
And that fixed my issue. Pretty obvious now!
I am using ng2 with webpack 2.
I cant figure out how to test component functions
Here is my component
import { Component, OnInit } from '#angular/core';
import { GlobalDataService } from '../global.service';
import { Router } from '#angular/router';
#Component({
selector: 'login',
templateUrl: './login.component.html'
})
export class LoginComponent {
constructor(private gd: GlobalDataService, private router: Router) { }
login(): void {
this.gd.shareObj['role'] = 'admin';
this.router.navigateByUrl('/login');
}
}
I would like to test login() function and see, if this.gd.shareObj['role'] = 'admin'; is truly set as admin.
What could .spec.ts file look like?
I would do it as follows:
class RouterStub {
navigateByUrl(url: String) { return url; }
}
class GlobalDataServiceStub {
shareObj: any = {};
}
describe('LoginComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [
{ provide: GlobalDataService, useClass: GlobalDataServiceStub },
{ provide: Router, useClass: RouterStub }
]
});
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance;
});
it('should set role to admin',
inject([GlobalDataService], (gd: GlobalDataService) => {
comp.login();
expect(gd.shareObj['role']).toBe('admin');
})
);
});
Plunker Example
I have a validator that checks if a users email address is unique, to do this I need to also pass in the users id so that it doesn't include itself in the unique checks. What is the best way to achieve this?
From what I can tell the validator only has access to the control value. I'm hooking up my validator like this:
<input #emailAddress="ngForm" type="text" [(ngModel)]="user.emailAddress" ngControl="emailAddress" required userExists />
Currently the only way I've been able to achieve it is by setting a static value on the validator, which is not ideal! Here's my full code for the validator:
import { NG_ASYNC_VALIDATORS, Control } from '#angular/common';
import { Directive, provide, forwardRef, Attribute } from '#angular/core';
import { UserService } from './user.service';
import { User } from './user.model';
interface ValidationResult {
[key: string]: boolean;
}
#Directive({
selector: '[userExists][ngModel]',
providers: [
provide(NG_ASYNC_VALIDATORS, {
useExisting: forwardRef(() => UserExistsValidator),
multi: true
})
]
})
export class UserExistsValidator {
public static user: User;
constructor(private _userService: UserService) { }
validate(control: Control): Promise<ValidationResult> {
return new Promise((resolve, reject) => {
this._userService.exists(control.value, UserExistsValidator.user.id).subscribe(
(response: any) => {
if (response.exists)
return resolve({ userExists: { valid: false } });
else
return resolve(null);
},
(error: any) => { console.log(error); }
)
});
}
}
I would use a shared service
#Injectable()
class ValidatorParam {
value:string; // could also be an observable
}
#Directive({
selector: '[userExists][ngModel]',
providers: [
{ provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UserExistsValidator),
multi: true
})
]
})
export class UserExistsValidator {
public static user: User;
constructor(private _userService: UserService, private _param:ValidatorParam) { }
validate(control: Control): Promise<ValidationResult> {
return new Promise((resolve, reject) => {
this._param.... // don't know what you want to do with it
this._userService.exists(control.value, UserExistsValidator.user.id).subscribe(
(response: any) => {
if (response.exists)
return resolve({ userExists: { valid: false } });
else
return resolve(null);
},
(error: any) => { console.log(error); }
)
});
}
}
#Component({
selector: '...',
providers: [ValidatorParam],
template: `
<input #emailAddress="ngForm" type="text" [(ngModel)]="user.emailAddress" ngControl="emailAddress" required userExists />
`})
export class MyComponent {
constructor(private _validatorParam:ValidatorParam) {
this._validatorParam.value = xxx;
}
}
This way you can only have one service per component. If you have several input elements in this component, then they need to share the service.
Caution: not tried myself.
I am building a basic CRUD application in Angular2. However I am having some issues while running tests of components.
Component Code:
///<reference path="../../node_modules/angular2/typings/browser.d.ts"/>
import { Component, OnInit } from 'angular2/core';
import { RouteParams, Router, ROUTER_DIRECTIVES } from 'angular2/router';
import { EmployeeEditFormComponent } from './employee-edit-form.component';
import { EmployeeDetailServiceComponent } from '../services/employee-detail-service.component';
import { EmployeeDeleteServiceComponent } from '../services/employee-delete-service.component';
#Component({
selector: 'employee-detail',
templateUrl: 'src/pages/employee-detail.component.html',
providers: [
EmployeeDetailServiceComponent,
EmployeeDeleteServiceComponent
],
directives: [ ROUTER_DIRECTIVES, EmployeeEditFormComponent ]
})
export class EmployeeDetailComponent implements OnInit {
public currentEmployee;
public errorMessage: string;
constructor(
private _router: Router,
private _routeParams: RouteParams,
private _detailService: EmployeeDetailServiceComponent,
private _deleteService: EmployeeDeleteServiceComponent
){}
ngOnInit() {
let id = parseInt(this._routeParams.get('id'));
this._detailService.getEmployee(id).subscribe(
employee => this.currentEmployee = employee,
error => this.errorMessage = <any>error
);
}
deleteHandler(id: number) {
this._deleteService.deleteEmployee(id).subscribe(
employee => this.currentEmployee = employee,
errorMessage => this.errorMessage = errorMessage,
() => this._router.navigate(['EmployeeList'])
)
}
}
Spec Code:
/// <reference path="../../typings/main/ambient/jasmine/jasmine.d.ts" />
import {
it,
describe,
expect,
TestComponentBuilder,
injectAsync,
setBaseTestProviders,
beforeEachProviders
} from "angular2/testing";
import {
Response,
XHRBackend,
ResponseOptions,
HTTP_PROVIDERS
} from "angular2/http";
import {
MockConnection,
MockBackend
} from "angular2/src/http/backends/mock_backend";
import {
TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS
} from "angular2/platform/testing/browser";
import {
Component,
provide
} from "angular2/core";
import {
RouteParams
} from 'angular2/router';
import 'rxjs/Rx';
import { Employee } from '../models/employee';
import { EmployeeDetailComponent } from './employee-detail.component';
import { EmployeeEditFormComponent } from './employee-edit-form.component';
import { EmployeeDetailServiceComponent } from '../services/employee-detail-service.component';
import { EmployeeDeleteServiceComponent } from '../services/employee-delete-service.component';
class MockDetailService{
public getEmployee (id: number) {
return new Employee(1, "Abhinav Mishra");
}
}
class MockDeleteService{
public deleteEmployee (id: number) {
return new Employee(1, "Abhinav Mishra");
}
}
describe('Employee Detail Component Tests', () => {
setBaseTestProviders(
TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS
);
beforeEachProviders(() => {
return [
HTTP_PROVIDERS,
provide(XHRBackend, {useClass: MockBackend}),
provide(RouteParams, { useValue: new RouteParams({ id: '1' }) }),
provide(EmployeeDetailServiceComponent, {useClass: MockDetailService}),
provide(EmployeeDeleteServiceComponent, {useClass: MockDeleteService})
]
});
it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb
.overrideProviders(EmployeeDetailComponent,
[
provide(EmployeeDetailServiceComponent, {useClass: MockDetailService}),
provide(EmployeeDeleteServiceComponent, {useClass: MockDeleteService})
]
)
.createAsync(EmployeeDetailComponent).then((componentFixture) => {
componentFixture.detectChanges();
expect(true).toBe(true);
});
}));
});
I keep getting following error:
Error: XHR error (404 Not Found) loading http://localhost:9876/angular2/router
at error (/home/abhi/Desktop/angular2-testing/node_modules/systemjs/dist/system.src.js:1026:16)
at XMLHttpRequest.xhr.onreadystatechange (/home/abhi/Desktop/angular2-testing/node_modules/systemjs/dist/system.src.js:1047:13)
at XMLHttpRequest.wrapFn [as _onreadystatechange] (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:771:30)
at ZoneDelegate.invokeTask (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:365:38)
at Zone.runTask (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:263:48)
at XMLHttpRequest.ZoneTask.invoke (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:431:34)
Would be great to have some feedbacks.
I have an Angular 2 component I am trying to put under test, but I am having trouble because the data is set in the ngOnInit function, so is not immediately available in the unit test.
user-view.component.ts:
import {Component, OnInit} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {User} from './user';
import {UserService} from './user.service';
#Component({
selector: 'user-view',
templateUrl: './components/users/view.html'
})
export class UserViewComponent implements OnInit {
public user: User;
constructor(
private _routeParams: RouteParams,
private _userService: UserService
) {}
ngOnInit() {
const id: number = parseInt(this._routeParams.get('id'));
this._userService
.getUser(id)
.then(user => {
console.info(user);
this.user = user;
});
}
}
user.service.ts:
import {Injectable} from 'angular2/core';
// mock-users is a static JS array
import {users} from './mock-users';
import {User} from './user';
#Injectable()
export class UserService {
getUsers() : Promise<User[]> {
return Promise.resolve(users);
}
getUser(id: number) : Promise<User> {
return Promise.resolve(users[id]);
}
}
user-view.component.spec.ts:
import {
beforeEachProviders,
describe,
expect,
it,
injectAsync,
TestComponentBuilder
} from 'angular2/testing';
import {provide} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {UserViewComponent} from './user-view.component';
import {UserService} from './user.service';
export function main() {
describe('User view component', () => {
beforeEachProviders(() => [
provide(RouteParams, { useValue: new RouteParams({ id: '0' }) }),
UserService
]);
it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(UserViewComponent)
.then((rootTC) => {
spyOn(console, 'info');
let uvDOMEl = rootTC.nativeElement;
rootTC.detectChanges();
expect(console.info).toHaveBeenCalledWith(0);
expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);
});
}));
});
}
The route param is getting passed correctly, but the view hasn't changed before the tests are run. How do I set up a test that happens after the promise in ngOnInit is resolved?
IMO the best solution for this use case is to just make a synchronous mock service . You can't use fakeAsync for this particular case because of the XHR call for templateUrl. And personally I don't think the "hack" to make ngOnInit return a promise is very elegant. And you should not have to call ngOnInit directly, as it should be called by the framework.
You should already be using mocks anyway, as you are only unit testing the component, and don't want to be dependent on the real service working correctly.
To make a service that is synchronous, simple return the service itself from whatever methods are being called. You can then add your then and catch (subscribe if you are using Observable) methods to the mock, so it acts like a Promise. For example
class MockService {
data;
error;
getData() {
return this;
}
then(callback) {
if (!this.error) {
callback(this.data);
}
return this;
}
catch(callback) {
if (this.error) {
callback(this.error);
}
}
setData(data) {
this.data = data;
}
setError(error) {
this.error = error;
}
}
This has a few benefits. For one it gives you a lot of control over the service during execution, so you can easily customize it's behavior. And of course it's all synchronous.
Here's another example.
A common thing you will see with components is the use of ActivatedRoute and subscribing to its params. This is asynchronous, and done inside the ngOnInit. What I tend to do with this is create a mock for both the ActivatedRoute and the params property. The params property will be a mock object and have some functionality that appears to the outside world like an observable.
export class MockParams {
subscription: Subscription;
error;
constructor(private _parameters?: {[key: string]: any}) {
this.subscription = new Subscription();
spyOn(this.subscription, 'unsubscribe');
}
get params(): MockParams {
return this;
}
subscribe(next: Function, error: Function): Subscription {
if (this._parameters && !this.error) {
next(this._parameters);
}
if (this.error) {
error(this.error);
}
return this.subscription;
}
}
export class MockActivatedRoute {
constructor(public params: MockParams) {}
}
You can see we have a subscribe method that behaves like an Observable#subscribe. Another thing we do is spy on the Subscription so that we can test that it is destroyed. In most cases you will have unsubscribed inside your ngOnDestroy. To set up these mocks in your test you can just do something like
let mockParams: MockParams;
beforeEach(() => {
mockParams = new MockParams({ id: 'one' });
TestBed.configureTestingModule({
imports: [ CommonModule ],
declarations: [ TestComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
]
});
});
Now all the params are set for the route, and we have access to the mock params so we can set the error, and also check the subscription spy to make sure its been unsubscribed from.
If you look at the tests below, you will see that they are all synchronous tests. No need for async or fakeAsync, and it passes with flying colors.
Here is the complete test (using RC6)
import { Component, OnInit, OnDestroy, DebugElement } from '#angular/core';
import { CommonModule } from '#angular/common';
import { ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TestBed, async } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
#Component({
template: `
<span *ngIf="id">{{ id }}</span>
<span *ngIf="error">{{ error }}</span>
`
})
export class TestComponent implements OnInit, OnDestroy {
id: string;
error: string;
subscription: Subscription;
constructor(private _route: ActivatedRoute) {}
ngOnInit() {
this.subscription = this._route.params.subscribe(
(params) => {
this.id = params['id'];
},
(error) => {
this.error = error;
}
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
export class MockParams {
subscription: Subscription;
error;
constructor(private _parameters?: {[key: string]: any}) {
this.subscription = new Subscription();
spyOn(this.subscription, 'unsubscribe');
}
get params(): MockParams {
return this;
}
subscribe(next: Function, error: Function): Subscription {
if (this._parameters && !this.error) {
next(this._parameters);
}
if (this.error) {
error(this.error);
}
return this.subscription;
}
}
export class MockActivatedRoute {
constructor(public params: MockParams) {}
}
describe('component: TestComponent', () => {
let mockParams: MockParams;
beforeEach(() => {
mockParams = new MockParams({ id: 'one' });
TestBed.configureTestingModule({
imports: [ CommonModule ],
declarations: [ TestComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
]
});
});
it('should set the id on success', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
let debugEl = fixture.debugElement;
let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
expect(spanEls.length).toBe(1);
expect(spanEls[0].nativeElement.innerHTML).toBe('one');
});
it('should set the error on failure', () => {
mockParams.error = 'Something went wrong';
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
let debugEl = fixture.debugElement;
let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
expect(spanEls.length).toBe(1);
expect(spanEls[0].nativeElement.innerHTML).toBe('Something went wrong');
});
it('should unsubscribe when component is destroyed', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
fixture.destroy();
expect(mockParams.subscription.unsubscribe).toHaveBeenCalled();
});
});
Return a Promise from #ngOnInit:
ngOnInit(): Promise<any> {
const id: number = parseInt(this._routeParams.get('id'));
return this._userService
.getUser(id)
.then(user => {
console.info(user);
this.user = user;
});
}
I ran into the same issue a few days back, and found this to be the most workable solution. As far as I can tell, it doesn't impact anywhere else in the application; since #ngOnInit has no specified return type in the source's TypeScript, I doubt anything in the source code is expecting a return value from that.
Link to OnInit: https://github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122
Edit
In your test, you'd return a new Promise:
it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
// Create a new Promise to allow greater control over when the test finishes
//
return new Promise((resolve, reject) => {
tcb.createAsync(UserViewComponent)
.then((rootTC) => {
// Call ngOnInit manually and put your test inside the callback
//
rootTC.debugElement.componentInstance.ngOnInit().then(() => {
spyOn(console, 'info');
let uvDOMEl = rootTC.nativeElement;
rootTC.detectChanges();
expect(console.info).toHaveBeenCalledWith(0);
expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);
// Test is done
//
resolve();
});
});
}));
}
I had the same issue, here is how I managed to fix it. I had to use fakeAsync and tick.
fakeAsync(
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb
.overrideProviders(UsersComponent, [
{ provide: UserService, useClass: MockUserService }
])
.createAsync(UsersComponent)
.then(fixture => {
fixture.autoDetectChanges(true);
let component = <UsersComponent>fixture.componentInstance;
component.ngOnInit();
flushMicrotasks();
let element = <HTMLElement>fixture.nativeElement;
let items = element.querySelectorAll('li');
console.log(items);
});
})
)