Binding input value in unit test - javascript

I have a simple Angular component with text input which uses ngModel binding. the component works fine, but not the unit-test.
helloworld.component.html:
<div>
<input [(ngModel)]="name" >
<p>{{msg}} {{name}}</p>
</div>
helloworld.component.ts:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'hello-world',
templateUrl: './helloworld.component.html',
styleUrls: ['./helloworld.component.css']
})
export class HelloWorldComponent {
#Input() name: string = "World";
#Input() msg: string = "Hello";
}
helloworld.component.spec.ts:
import { async, ComponentFixture, TestBed, fakeAsync, tick } from
'#angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '#angular/core';
import { HelloWorldComponent } from './helloworld.component';
describe('HelloWorldComponent', () => {
let component: HelloWorldComponent;
let fixture: ComponentFixture<HelloWorldComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HelloWorldComponent ],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HelloWorldComponent);
component = fixture.componentInstance;
});
it('should print the name on input', ()=>{
component.msg = "Hi";
fixture.detectChanges();
var nameInput: HTMLInputElement = fixture.nativeElement.querySelector('input');
var label: HTMLElement = fixture.nativeElement.querySelector('p');
nameInput.value = 'John';
nameInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.msg).toBe('Hi');
expect(component.name).toBe('John');
expect(label.textContent).toBe('Hi John');
});
});
The test fails because name is not set to 'John' but still has the default value 'World'. The provided code is in my eyes equivalent to the example in Angular documentation (https://angular.io/guide/testing#component-binding)
There are some articles that explain to perform the test in async() and use fixture.whenStable().then() after changing the input value.
I also found the hint to run the test inside fakeAsync() and use tick() to wait for the input value to be bound to name. Both didn't help.
What am I missing?

Related

await for promise before give the result as input parameter

I have two components parent and child
parent is resolving a promise in constructor, child is reveiving the result of the promise as #Input() parameter
child is not receiving the result of the promise in life cicle hook other than afterViewCheck and afterContentCheck, I want to avoid those.
I also want to avoid a shared service containing shared data in behaviorSubject or something like that
so the question is, can I await the promise before construct the template with te result of the promise as an input parameter?
Parent:
// html: <app-chil [brands]="brands"></>
brands: Brand[] = []
constructor() {
this.getBrands()
}
async getBrands() {
this.brands = await new Promise<Brand[]>(resolve =>
this.equipmentService.getBrands().subscribe(brands => resolve(brands)))
}
child:
#Input() brands: Brand[] = []
constructor() { }
ngAfterViewInit() {
console.log(this.brands) // receive brands here
}
With a setter, you dont need to use any life cycle hook and its cleaner.
brands: Brand[] = [];
constructor() {
this.getBrands()
}
async getBrands() {
this.brands = await new Promise<Brand[]>(resolve =>
this.equipmentService.getBrands().subscribe(brands => resolve(brands)))
}
child:
private _brands: Brand[] = [];
#Input() set brands(data: Brand[]) {
this._brands = data;
console.log(data);
}
constructor() { }
Make observable from promise and pass that observable to the child and then use simple interpolation with async pipe.
Here goes an example. Read it and adapt it to your app (it's tested as is).
child:
import { Component, Input } from '#angular/core';
import {Observable} from 'rxjs';
#Component({
selector: 'hello',
template: `<h1>Hello {{name | async}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
#Input() name: Observable<string[]>;
}
parent:
import { Component } from '#angular/core';
import { of } from 'rxjs';
/* import { from } from 'rxjs'; //import 'from' to convert your promise to observable*/
#Component({
selector: 'my-app',
template: `<hello [name]="name"></hello>`,
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = of("angular"); //name = from(yourPromise)
}

ngOnInit and MediaChange testing with Karma

I am trying to test the code in the ngOnInit method. The code watches for change in screen size for a navigation bar to resize down to mobile or to stay as a top bar. I have tried a for about a week and keep getting a slew of different errors when I test. I have left out some code for comp.component.ts as the other code is not necessary for this. I keep getting subscribe is not a method or Can't resolve all parameters for MediaChange: (?, ?, ?, ?). Any advice on how I can achieve writing a test for this or any resources you might suggest looking at to help me figure this out.
comp.component.ts
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs';
import { MediaChange, ObservableMedia } from '#angular/flex-layout';
#Component({
selector: 'app-comp',
templateUrl: './comp.component.html',
styleUrls: ['./comp.component.scss']
})
export class NavigationComponent implements OnInit {
isOpen: Boolean;
watcher: Subscription;
activeMediaQuery = "";
media: ObservableMedia;
constructor() {
this.isOpen = false;
}
ngOnInit(): void {
this.watcher = this.media.subscribe((change: MediaChange) => {
this.activeMediaQuery = change ? `'${change.mqAlias}' = (${change.mediaQuery})` : '';
this.isOpen = false;
});
}
navPressed(event, path): void {
this.navClick.emit(path);
if ( this.checkSize() ) this.toggle();
}
checkSize(): Boolean {
return this.activeMediaQuery.includes('xs') || this.activeMediaQuery.includes('sm');
}
}
comp.component.spec.ts
import { Component } from '#angular/core';
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { DebugElement } from '#angular/core';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { MatButtonModule, MatToolbarModule, MatIconModule } from '#angular/material';
import { CompComponent } from './comp.component';
import { Subscription } from 'rxjs';
import { MediaChange, ObservableMedia } from '#angular/flex-layout';
#Component({
selector: 'app-test-component-wrapper',
template: '<app-navigation [navItems]="clickables" (navClick)="handleNavClick($event)"></app-navigation>'
})
class TestWrapperComponent {
clickables = [
{ path: '/login', label: 'Login', onClick() {} }
];
}
describe('app testing', () => {
let component: CompComponent;
let fixture: ComponentFixture<TestWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatToolbarModule,
MatIconModule,
BrowserAnimationsModule
],
declarations: [
TestWrapperComponent,
NavigationComponent
],
providers: [
ObservableMedia,
MediaChange,
Subscription
]
}).compileComponents();
fixture = TestBed.createComponent(TestWrapperComponent);
}));
it('should create and have Login label', () => {
// EDIT START
spyOn(ObservableMedia, 'prototype');
// EDIT END
expect(fixture).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => {
component = fixture.debugElement.children[0].componentInstance;
expect(component.navItems[0].label).toBe('Login');
});
});
});
EDIT: Added the 'EDIT' comment in the code with the code I have added. I am now getting the resolve all parameters for MediaChange: (?, ?, ?, ?) error which I think is forward progress from the subscribe error mentioned above.
Some observations:
ObservableMedia from flex-layout needs to be injected into your component to work. Details here
You aren't providing MediaChange or Subscription in the original component, so no need to in the TestBed either.
In the stackblitz below I had to make a few assumptions. Let me know if any of these are wrong, or just go ahead and update the stackblitz:
In your spec you imported CompComponent, but in comp.component.ts you defined NavigationComponent. Of the two I chose to use NavigationComponent.
navClick was missing from your code above, so I assumed it is an #Output from your component (since you emit a path to it).
navItems was also missing from the code above, but since you are testing it I assumed it was important and guessed it is an input to your component (again, just by the way you were using it).
You didn't include your template, so I mocked it very simply.
toggle was called from within navPressed, but didn't exist so I created it as an empty function.
Here is the stackblitz: https://stackblitz.com/edit/stackoverflow-q-53024049?file=app%2Fmy.component.spec.ts
To fix what you had: I made the changes above and mocked the ObservableMedia object passed in with the following:
let mockFlex = jasmine.createSpyObj({
subscribe: ({mqAlias: 'xs', mediaQuery: ''}),
isActive: true,
});
I also changed the providers array to the following:
providers: [
{ provide: ObservableMedia, useValue: mockFlex }
]
Check the stackblitz for all the details. As you can see there, the test now passes.

Error in Angular

I have this component in angular:
import { Component, AfterViewInit, ElementRef, ViewChild} from '#angular/core';
#Component({
selector: 'jhi-login-modal',
templateUrl: './login123.component.html'
})
export class JhiLoginModalComponent implements AfterViewInit {
a: number;
#ViewChild('hiddenLabel') hidden: ElementRef;
ngAfterViewInit() {
const a = 5;
console.log('Print a' + a);
console.log('Print htmlcontent' + this.hidden);
}
}
My HTML file is like this:
<label #hiddenLabel>Visible!</label>
And then I have a testing file for these where I want to test the html element. But firstly I want to make sure that I can print it and then test the value. Here is my test file:
import { ComponentFixture, TestBed, async, inject, fakeAsync, tick } from '#angular/core/testing';
import { JhiLoginModalComponent } from '../../../../../../main/webapp/app/shared/login/login.component';
describe('Component Tests', () => {
describe('LoginComponent', () => {
let comp: JhiLoginModalComponent;
let fixture: ComponentFixture<JhiLoginModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [JhiLoginModalComponent]
})
.overrideTemplate(JhiLoginModalComponent, '')
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(JhiLoginModalComponent);
comp = fixture.componentInstance;
});
it ('first test', async(() => {
console.log('holaaa');
fixture.detectChanges();
}));
});
});
What I get as a result in my console is that test passed. Normally this should be ok. And then it prints holaaa and then print a5 which is again alright. Then it prints Print htmlcontentundefined instead of printing Visible. What is the difference between teh number and teh HTMLElement?? Is there another way to reference the HTML elements?? As much as I have read on this it should be this way.
Thanks!
It seems that your hiddenLabel child is undefined, meaning it is not being picked up by your component typescript spec file. This is due to the .overrideTemplate(JhiLoginModalComponent, ''). This line replaces your JhiLoginModalComponent template with nothing. Thus, when your JhiLoginModalComponent trys to get the hiddenLabel viewChild it cannot find anything, therefore it is undefined.
You should change your spec to look like the following:
import { ComponentFixture, TestBed, async, inject, fakeAsync, tick } from '#angular/core/testing';
import { JhiLoginModalComponent } from '../../../../../../main/webapp/app/shared/login/login.component';
describe('Component Tests', () => {
describe('LoginComponent', () => {
let comp: JhiLoginModalComponent;
let fixture: ComponentFixture<JhiLoginModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [JhiLoginModalComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(JhiLoginModalComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
it ('first test', async(() => {
console.log('holaaa');
fixture.detectChanges();
}));
});
});

Using #ViewChild { read: ElementRef } of component causes unit test to fail

In my component I have a child component that looks like this:
<child-component #childComponent></childComponent>
In my parent component I then access this child component using #ViewChild and the read parameter to get the ElementRef, and not the component reference. I need the ElementRef to ensure I can get some properties from nativeElement that I need. So it's like this:
export class ParentComponent {
#ViewChild('childComponent', { read: ElementRef }) public childComponent: ElementRef;
public position: string;
// some way down the code
private someMethod() {
if (this.childComponent.nativeElement.offsetLeft > 500) {
this.position = 'left';
} else {
this.position = 'right';
}
}
}
So this works for the application, however I am writing the tests and mocking the child component, like this:
#Component({
selector: 'child-component',
template: ''
})
class ChildComponentMockComponent {
private nativeElement = {
get offsetLeft() {
return 600
}
};
}
beforeEach(async(() => TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ParentComponent, ChildComponentMockComponent ],
providers: [ ... ],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()));
it('should have the correct position, based on position of child-component', () => {
spyOn(component, 'someMethod');
expect(component.someMethod).toHaveBeenCalled();
expect(component.position).toBe('left');
});
So the test will compile the component, and use the mocked child component values as the proper value and compute the value of this.position, which is then asserted in the test.
However, when the { read: ElementRef } parameter is set, the mock gets completely ignored by the TestBed, even though it's being added in the declarations array. If I remove { read: ElementRef }, the mock is used in the test and it passes. But then my application doesn't work, as it is now getting the component reference, where the nativeElement property doesn't exist, rather than the element reference.
So how do I get the ElementRef in my application and then in my test use the mock component?
I have fixed this by changing the architecture of the app. The child component now finds it's own offsetLeft property, and then puts it into an output EventEmitter to be picked up the parent component.
export class ChildComponent implements AfterViewInit {
#Output() offsetPosition: EventEmitter<number> = new EventEmitter<number>();
constructor(private el: ElementRef) {}
public ngAfterViewInit() {
this.offsetPosition.emit(this.el.nativeElement.offsetLeft);
}
}
export class ParentComponent implements AfterViewInit {
public childComponentOffset: number;
public ngAfterViewInit() {
setTimeout(() => {
// this.childComponentOffset is available
// needs to be in setTimeout to prevent ExpressionChangedAfterItHasBeenCheckedError
// more info: https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4
}
}
public getChildComponentOffset(position: number): void {
this.childComponentOffset = position;
}
}
And then in the HTML, you just define the child component with output variable and method:
<child-component (offsetPosition)="getChildComponentOffset($event)"></child-component>
In the test, I then mock ElementRef for the child component and use it as a provider.
const mockElementRef: any = {
get offsetLeft() {
return position;
}
};
beforeEach(async(() => TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ParentComponent ],
providers: [
{ provide: ElementRef, useValue: mockElementRef }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()));
it('should have the correct position, based on position of child-component', (done) => {
component.getChildComponentOffset(600);
setTimeout(() => expect(component.position).toBe('left'));
done();
});

Unit test error: Cannot call Promise.then from within a sync test

I started looking into unit testing angular 2 applications, but I'm stuck even in the simplest examples. I just want to run a simple test to see if it even works, basically what I want is to compare a value from the title page to the one in the test.
This is the error I'm getting, but I don't see where the error is coming from since everything looks to be synchronous to me.
Error: Error: Cannot call Promise.then from within a sync test.
Unit test:
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement, Input} from '#angular/core';
import { ToDoComponent } from './todo.component';
import { FormsModule } from '#angular/forms';
describe(("test input "),() => {
let comp: ToDoComponent;
let fixture: ComponentFixture<ToDoComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ ToDoComponent ],
imports: [ FormsModule ]
})
.compileComponents();
});
fixture = TestBed.createComponent(ToDoComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css("h1"));
el = de.nativeElement;
it('should display a different test title', () => {
comp.pageTitle = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toBe('Test Title423');
});
});
My component:
import {Component} from "#angular/core";
import {Note} from "app/note";
#Component({
selector : "toDoArea",
templateUrl : "todo.component.html"
})
export class ToDoComponent{
pageTitle : string = "Test";
noteText : string ="";
noteArray : Note[] = [];
counter : number = 1;
removeCount : number = 1;
addNote() : void {
if (this.noteText.length > 0){
var a = this.noteText;
var n1 : Note = new Note();
n1.noteText = a;
n1.noteId = this.counter;
this.counter = this.counter + 1;
this.noteText = "";
this.noteArray.push(n1);
}
}
removeNote(selectedNote : Note) :void{
this.noteArray.splice(this.noteArray.indexOf(selectedNote),this.removeCount);
}
}
Move your variable initialization inside a beforeEach.
You shouldn't be getting things out of the TestBed or managing the fixture or component in the describe scope. You should only do these things within the scope of a test run: inside a beforeEach/beforeAll, afterEach/afterAll, or inside an it.
describe(("test input "), () => {
let comp: ToDoComponent;
let fixture: ComponentFixture<ToDoComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ToDoComponent],
imports: [FormsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ToDoComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css("h1"));
el = de.nativeElement;
})
it('should display a different test title', () => {
comp.pageTitle = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toBe('Test Title423');
});
});
See also
https://angular.io/docs/ts/latest/guide/testing.html#!#waiting-compile-components
I got the same error for a different reason. I put a TestBed.get(Dependency) call within a describe block. The fix was moving it to the it block.
Wrong:
describe('someFunction', () => {
const dependency = TestBed.get(Dependency); // this was causing the error
it('should not fail', () => {
someFunction(dependency);
});
});
Fixed:
describe('someFunction', () => {
it('should not fail', () => {
const dependency = TestBed.get(Dependency); // putting it here fixed the issue
someFunction(dependency);
});
});

Categories