I am testing this html form:
<input #nhcInput type="text" class="form-control" name="nhc" id="field_nhc"
[(ngModel)]="paciente.nhc" maxlength="38" pattern="[0-9]+"/>
<div [hidden]="!(editForm.controls.nhc?.dirty && editForm.controls.nhc?.invalid)">
<small class="form-text text-danger"
[hidden]="!editForm.controls.nhc?.errors?.maxlength" jhiTranslate="entity.validation.maxlength" translateValues="{ max: 38 }">
This field cannot be longer than 38 characters.
</small>
TEH RESULT {{nhcInput.className}} //This line prints ng-valid/ dirty, touched correctly
I have this in my component:
paciente: Paciente = {nhc: '23423' } as Paciente;
it ('NHC cannot have more than 38 characters', async(() => {
comp.paciente.nhc = 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr' ;
console.log(fixture.nativeElement.querySelector('input[name="nhc"]').className);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input[name="nhc"]').className.includes('ng-invalid')).toEqual(true);
}));
Now I want to test the validity by checking the vaidator.
The console.log prints out only form-control without the type of validator, since it is not finding it.
I put a validator for this fild like this in my component:
#ViewChild('editForm') editForm: any;
editform.controls["nhc"].setValidators([ Validators.maxLength(38)]);
But this doesnt work. Am I doing soemthing wrong here?
Thanks!
Your issue comes from the fact that you don't do things in order and don't rely on the framework to make your tests.
I made a sandbox with a working test, feel free to look at it. Now for the explanation :
Let's start with the component :
#Component({
selector: 'hello',
template: `
<form #myForm="ngForm">
<input type="text" maxlength="20" name="patient" id="patient" [(ngModel)]="patient">
</form>
`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
#ViewChild('myForm') myForm: NgForm;
patient: string;
}
A very simple component with a template driven form and a basic validation and binding.
If you do this
ngOnInit() {
console.log(this.myForm.controls);
setTimeout(() => console.log(this.myForm.controls));
}
You will see both undefined and { patient: FormControl }. This happens because you don't wait for the view to be initialized before doing your tests. This means that the test can't find the form control, and hence can't pass.
Now for the test itself :
import { Component } from '#angular/core';
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { HelloComponent } from './hello.component';
import { FormsModule } from '#angular/forms';
describe('HelloComponent', () => {
let fixture: ComponentFixture<HelloComponent>;
let component: HelloComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [HelloComponent]
});
fixture = TestBed.createComponent(HelloComponent);
component = fixture.debugElement.children[0].componentInstance;
fixture.detectChanges();
});
it('should be invalid with more than 20 chars', async(() => {
setTimeout(() => {
component.myForm.controls['patient'].setValue('ddddddddddddddddddddd'); // 21
fixture.detectChanges();
expect(component.myForm.control.invalid).toEqual(true);
});
}));
});
the start is very basic, test bed is configured and changes are detected.
Now comes the part where you test : you first must wait for the component to load with a timeout, then, you need to set the value on the form by using the framework :
component.myForm.controls['patient'].setValue('ddddddddddddddddddddd'); // 21
This inputs 21 d into the input, which is making it invalid. After that, you need to trigger the changes detection, and now you can make your expectation with
expect(component.myForm.control.invalid).toEqual(true);
This will take the form as a control, meaning it has all the properties and functions a FormControl has. Among these, you can find the invalid property, which states if your control is in an invalid state.
Again, I have to state that this kind of test is useless, because you basically try to see if the Framework is working properly. That's not your job, that's Angular team's job. If you want to test something, you should test that the form can't be submitted when it has an invalid state, which is a business rule (I guess) and prevents side effects (unlike this test).
Related
I'm attempting to listen to changes on a reactive email form control like this:
import { Component, OnChanges } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnChanges {
form: FormGroup = new FormGroup({
email: new FormControl('',[ Validators.email ])
});
get emailInput() { return this.form.get('email'); }
ngOnChanges() {
this.form.get('email').valueChanges.subscribe(val => {
const formattedMessage = `Email is ${val}.`;
console.log(formattedMessage);
});
}
}
The form looks like this:
<form [formGroup]="form">
<input placeholder="Email" type="email" formControlName="email" >
</form>
When typing in the email field nothing gets logged. This is the Stackblitz. Thoughts?
This is the article the question implementation was based on.
Update
The accepted answer is to use the ngOnInitit lifecycle hook. I wanted if perhaps it should be ngAfterViewInit just to make sure the view is entirely initialized or will be form bindings always be complete in ngOnInit?
Didn't notice at first, but your ngOnChanges should not be where you are subscribing to the observable. ngOnChanges is for changes to input parameters to the current component (typically wrapped in []).
Setup your subscription to the observable in the ngOnInit like this and your code will work:
ngOnInit() {
this.emailSubscription = this.form.get('email').valueChanges.subscribe(val => {
const formattedMessage = `Email is ${val}.`;
console.log(formattedMessage);
});
}
Angular does not automatically unsubscribe so typically you'll want to save the value of the description, and then unsubscribe it in the ngOnDestroy:
ngOnDestroy() {
this.emailSubscription.unsubscribe();
}
Since you're writing this code in appComponent there's probably not an explicit need to do this outside it being generally good practice for every other component.
Edit: Updated stackblitz showing this working.
You're using onChanges wrong. OnChanges watches for changes performed on a child component so that the parent component can update information. You're doing this with a form, so nothing will send changes up to the component.
Since the input is an element on the component, you can do it with an (input) listener or a (keypress).
I hope this question clarifies so many people about testing template driven forms that Angular documentation barely mentions. I have an input inside this form that I want to test.
I want to test a simple DOM in Angular using Jasmine. This is a template driven form using NgForm like this:
<form name="editForm" role="form" novalidate (ngSubmit)="save()" #editForm="ngForm">
<input #inputtotest type="text" class="form-control" name="nombre" id="field_nombre"
[(ngModel)]="paciente.nombre" required/>
I want to test only:
If this input for this label sends data (paciente.nombre) using NgModel or better if its Property is paciente.model.
According to this responses we can use ViewChild for that: use of viewchild on DOM How to get ngForm variable reference in the component class
My component file is like this:
//all the other imports
import { ViewChild } from '#angular/core';
#Component({
...
})
export class PacienteDialogComponent implements OnInit
{
#ViewChild('inputtotest') inputtotest: **ElementRef**; //Create a viewchild of my input s I can obtain its value
paciente: Paciente; //Paciente is a modal that initializes the nombre
...
}
And my spec file to write the tests for the form is like this:
//All te Angular imports
describe('Component Tests', () => {
describe('Paciente Management Dialog Component', () => {
let comp: PacienteDialogComponent;
let fixture: ComponentFixture<PacienteDialogComponent>;
//starts my code
let input: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
.....angular imports
}));
beforeEach(() => {
fixture = TestBed.createComponent(PacienteDialogComponent);
comp = fixture.componentInstance;
input = `fixture.debugElement.query(By.css('inputtotest')).nativeElement;`
});
Tests:
it ('The form should call save method on submit', async(() => {
fixture.detectChanges();
spyOn(comp,'save');
savebutton.click();
expect(comp.save).toHaveBeenCalledTimes(0);
}));
it ('The form should have the input for paciente.nombre', async(() => {
fixture.detectChanges();
expect(inputname.getAttribute.value).toEqual(paciente.nombre);
}));
Now I have obtained the input as a HTMLelement and I do not find any way on how to check that it is Equal to ngModel value that is paciente.nombre. I do it like in the example and the result is the error:
Property value does not exist in type name string.
Have I done something wrong?
So, this is my use case: I get an observable from rest API, then by using async within the template I foreach the data. On click, I get the ID of particular user and then fill-in the form with the data of that user. To achieve the latter, I reuse the existing observable and filter the data, then subscribe to it. I was wondering if my approach is correct, since I believe that app is too slow when I click "edit" for the form to get filled, so I'm guessing here that I create too many subscriptions or whatnot?
Also, for the sake of argument, I created two observables (user and users) and one (user) gets a "reference" to another (users), then it is displayed in the template with async, while I also subscribe to it and set it to a "regular" variable. Is this an issue? To display an observable with async and also subscribing to it?
Here's the code, since my question might be a bit confusing:
//our root app component
import {Component, NgModule, VERSION, OnInit, OnDestroy} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import { HttpClientModule } from '#angular/common/http';
import { ReactiveFormsModule } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/find';
import 'rxjs/add/operator/takeUntil';
import { FormArray, FormBuilder, FormGroup, Validators } from '#angular/forms';
import { UserService } from './service';
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
<div>
{{user | async | json}}
{{selected | json}}
</div>
<div *ngFor="let u of users | async">
{{u.first_name}} {{u.last_name}}
<br />
<img [src]="u.avatar" />
<br />
<br />
<a (click)="edit(u.id)">Edit</a>
<br />
<br />
</div>
<div>
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" novalidate>
<input type="text" formControlName="first_name" />
<input type="text" formControlName="last_name" />
</form>
<p>userForm value: {{ userForm.value | json}}</p>
<p>validity: {{ userForm.valid }}</p>
</div>
</div>
`,
styles: ['a { cursor: pointer; }']
})
export class App implements OnInit, OnDestroy {
users: Observable<any>;
user: Observable<any>;
destroy$: Subject<boolean> = new Subject<boolean>();
name:string;
userForm: FormGroup;
constructor(private fb: FormBuilder, private service: UserService) {
this.name = `Angular! v${VERSION.full}`;
this.createForm();
}
createForm() {
this.userForm = this.fb.group({
first_name: ['', {updateOn: 'blur', validators: [Validators.required, Validators.minLength(2), Validators.maxLength(10)]}],
last_name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(20)]]
})
}
edit(id) {
this.user = this.users.map(x => x.find(x => x.id === +id));
this.user.takeUntil(this.destroy$).subscribe(u => {
console.log(u);
this.selected = u;
this.userForm.patchValue({
first_name: u.first_name,
last_name: u.last_name
});
});
}
ngOnInit() {
console.clear();
console.log('hello world');
this.users = this.service.all();
}
ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.unsubscribe();
}
}
#NgModule({
imports: [ BrowserModule, HttpClientModule, ReactiveFormsModule ],
providers: [UserService],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
and here's the link: http://plnkr.co/edit/F0dheIdgoNNw53hNVjZm?p=preview
Let's say you want to use observables for this task. Although there is more straightforward method in the answer from Leon but for sake of learning about Observables let's do it :-)
You are subscribing to user observable in the template using async pipe but also in edit method. The subscription in edit method is never unsubscribed so there is a memory leak.
Also you redefine this.user observable each time you click on user and letting async pipe in template to resubscribe. This results in another memory leak since async pipe has no clue about the overriden observable which should be unsubscribed.
This is not really how you should compose your observable streams. First you should define the observable data transformation.
When I look on your application you have two sources of data/events:
users from service
clicks on edit button
Users are just observable from a service. Clicks should be really Subject. Because on edit you should emit the userId which was clicked (observable) and react on this click by displaying user detail (observer behaviour).
editClick: Subject<number> = new Subject<number>;
So let's define how user observable should look like (all this code in ngOnInit):
this.users = this.service.all().shareReplay();
this.user = this.editClick
.switchMapTo(this.users, (userId, users) => ({ userId, users}))
.map(x => x.users.find(user => user.id === +x.userId))
.do((u) => {
console.log(u);
this.selected = u;
this.userForm.patchValue({
first_name: u.first_name,
last_name: u.last_name
});
});
Here I define user observable based on editClick - thats where I will get id from later. Using switchMapTo I combine users observable and id from editClick into object which I use later to filter out clicked user.
Now how to trigger editClick in onEdit method? Just emit userId on each click like this:
edit(id) {
this.editClick.next(id);
}
Notice how I define data stream in ngOnInit. Just once at beginning. No recreation or something. I have two observables to subscribe:
users to display users list
user to display detail - this one will emit each time I trigger editClick
Also there is only subscription in template therefore no memory leaks since async pipe will manage unsubscribe for you.
You can greatly simplify the edit method by passing the whole user instead of just the id.
<a (click)="edit(u)">Edit</a>
edit(user) {
this.selected = user;
this.userForm.patchValue({
first_name: user.first_name,
last_name: user.last_name
});
}
As for calling service.all() multiple times will cause multiple subscriptions and multiple calls to the backend.
If you want to call it multiple times use the .share() rxjs operator.
That will create an wrapper subject around the observable which will return the same value each time it is called.
I have created a very simple directive to use on input elements, that should only allow entry of a decimal number (numeric with single decimal point).
The directive is defined as follows:
import { HostListener, Directive, ElementRef } from '#angular/core';
// Directive attribute to stop any input, other than a decimal number.
#Directive({
selector: '[decimalinput]'
})
export class DecimalInputDirective {
constructor(private element : ElementRef) { }
// Hook into the key press event.
#HostListener('keypress', ['$event']) onkeypress( keyEvent : KeyboardEvent ) : boolean {
// Check if a full stop already exists in the input.
var alreadyHasFullStop = this.element.nativeElement.value.indexOf('.') != -1;
// Get the key that was pressed in order to check it against the regEx.
let input = String.fromCharCode(keyEvent.which);
// Test for allowed character using regEx. Allowed is number or decimal.
var isAllowed = /^(\d+)?([.]?\d{0,2})?$/.test( input );
// If this is an invlid character (i.e. alpha or symbol) OR we already have a full stop, prevent key press.
if (!isAllowed || (isAllowed && input == '.' && alreadyHasFullStop)){
keyEvent.preventDefault();
return false;
}
return true;
}
}
This directive should allow "123.123" not "abc", nor "1.2.1". Now I want to test this directive, reading online, I've come up with this so far:
import { Component, OnInit, TemplateRef,DebugElement, ComponentFactory, ViewChild, ViewContainerRef } from '#angular/core';
import { TestBed, ComponentFixture } from '#angular/core/testing';
import { DecimalInputDirective } from './decimalinput.directive';
import { By } from '#angular/platform-browser';
#Component({
template: `<input type="text" name="txtDecimalTest" decimalinput>`
})
class TestDecimalComponent { }
describe('Directive: DecimalInputDirective', () => {
let component: TestDecimalComponent;
let fixture: ComponentFixture<TestDecimalComponent>;
let decimalInput: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestDecimalComponent]
});
fixture = TestBed.createComponent(TestDecimalComponent);
component = fixture.componentInstance;
decimalInput = fixture.debugElement.query(By.css('input[name=txtDecimalTest]'));
});
it('Entering email and password emits loggedIn event', () => {
// This sets the value (I can even put "abc" here and it will work.
decimalInput.nativeElement.value = "12345";
// But I am trying to initialize the keypress event, so the character is tested in a real world way when the user is using.
decimalInput.nativeElement.dispatchEvent(new KeyboardEvent("keypress", { key: "a" })); // Nothing happens here! This was my attempt...
// This
expect(decimalInput.nativeElement.value).toBe("12345");
});
});
You can see from the code, the line:
decimalInput.nativeElement.dispatchEvent(new KeyboardEvent...
Is my attempt to simulate keypresses, as if the user was inputting. If I simulated a, then b, then c, then 1, then 2, then 3, I'd expect the test to make sure the value is only "123" and its ignored "abc" in the way the directive works.
Two questions - 1) is this the correct test I should be doing? 2) Whats wrong with my code - why is the simulated key press doing nothing?
Thanks for any pointers in advance! :)
Normally directive are tested in such a way that its being used in real component. So you can create a fake component which will use your directive and you can test that component to handle your directive.
This is the most people suggest.
So in your test file create a fake directive
// tslint:disable-next-line:data
#Component({
selector: 'sd-test-layout',
template: `
<div sdAuthorized [permission]="'data_objects'">test</div>`
})
export class TestDecimalInputDirectiveComponent {
#Input() permission;
constructor() {
}
}
Then in your before each using TestBed create the component instance, and now You are ready to apply mouse events and test them in real situation
TestBed.configureTestingModule({
imports: [
HttpModule,
SharedModule
],
declarations: [
TestDecimalInputDirectiveComponent,
],
providers: [
{
provide: ElementRef,
useClass: MockElementRef
},
AuthorizedDirective,
]
}).compileComponents();
Just given you the hint. you can follow this link to get more information
I'm got a couple of (beginner?) problems with my Angular2 app. Firstly a DI works for one component, but not another, and I cant see any problem.
I've got a cocktail component that includes a starring functionality, and a list of ingredients. All code below is stripped down, and it all runs fine (no console errors) - I'm just not getting the desired output
Heres the cocktail.component.ts:
import { Component } from '#angular/core'
import { CocktailService } from './cocktail.service'
import { HttpModule } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Component({
selector: 'cocktails',
templateUrl: 'app/cocktail.template.html',
providers: [CocktailService, HttpModule]
})
export class CocktailsComponent {
title: string = "Sunday Brunch Specials";
cocktails;
isStarred = false;
numLikes = 10;
ingredient_json = "Bob"; // for testing
constructor(private _cocktailService: CocktailService) {
this.cocktails = _cocktailService.getCocktails();
}
}
And the cocktail.template.html....
<h2>{{title}}</h2>
<input type="text" autoGrow />
<ul>
<li *ngFor="let cocktail of cocktails | async">
<h3>{{cocktail.name}}</h3>
<star [is-starred]="isStarred" [num-likes]="numLikes" (change)="onStarredChange($event)"></star>
<ingredients [ingredient-json]="ingredient_json"></ingredients>
<li>
The star component is getting passed isStarred and numLikes properly - all good.
Now in the ingredient component:
import {Component, Input} from '#angular/core';
import {IngredientService} from './ingredient.service';
#Component({
selector: 'ingredients',
template: '
<h4>Ingredients</h4>
<ul>
<li *ngFor="let ingredient of ingredients">{{ingredient}}</li>
</ul>'
)}
export class IngredientsComponent {
#Input('ingredient-json') ingredient_json = "Hello";
title: 'Ingredients';
ingredients;
constructor() {
// Why isn't ingredient_json outputting anything but hello? Ie nothing is going into input
console.log("Ingredient JSON = " + this.ingredient_json);
}
}
The problem I've stated in comments. The ingredient_json variable just isn't instantiated from the #Input. I'm not getting any errors, the console just always prints out 'Hello' - ie the default. The #Input isn't grabbing anything from its parent (cocktail.component.ts).
I havent included all the app, as I feel as though something is amiss in these 3 files. Like I said, the DI works on the component, so I know that the DI works, but I can't see whats wrong in my code below.
Thanks a lot
Constructor is invoked before the #input() params are passed to a component.
Implement the method ngOnChanges() in your IngredientsComponent and move your console.log() there.
Details here: https://angular.io/docs/ts/latest/api/core/index/OnChanges-class.html