Cannot read properties of null (reading 'nativeElement') - Test Angular - javascript

I'm new to Angular and I'm trying to perform a test... I want to test my function in component.ts that receives an event through click, and this value passes to an output for the component dad.
The test code looks like this...
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { QuadradosComponent } from './quadrados.component';
describe('QuadradosComponent', () => {
let component: QuadradosComponent;
let fixture: ComponentFixture<QuadradosComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ QuadradosComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(QuadradosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('Deve emitir mensagem quando clicar na div', () => {
const emitMessageSpy = spyOn(component.eventoOutput, 'emit')
let divClick = fixture.debugElement.query(By.css('.casa')).nativeElement
divClick.click()
expect(emitMessageSpy).toHaveBeenCalled
})
});
The component.ts
import { Component, EventEmitter, Input, Output, OnInit} from '#angular/core';
import { JogadasService } from 'src/app/services/jogadas.service';
#Component({
selector: 'app-quadrados',
templateUrl: './quadrados.component.html',
styleUrls: ['./quadrados.component.css']
})
export class QuadradosComponent implements OnInit{
constructor(public service: JogadasService) {}
ngOnInit(): void {
this.service.emitirVitoria.subscribe(
array => this.vitoria.push(...array)
)
console.log(document.getElementsByClassName('casa'))
}
vitoria: number[] =[];
posicoes: number = 0;
#Input() quadrados?: Array<number>;
#Output() eventoOutput = new EventEmitter();
#Output() index = new EventEmitter();
eventoClick(evento: MouseEvent) {
this.eventoOutput.emit(evento);
console.log(document.getElementsByClassName('casa'))
}
pegaIndex(index: number) {
this.posicoes = index;
this.index.emit(this.posicoes);
}
vitoriaArray(modelo: number) {
for(let i = 0; i< this.vitoria.length; i++) {
if(this.vitoria[i] == modelo) {
return this.vitoria[i]
}
}
}
}
the component HTML:
<div class="centraliza">
<div class="jogo-da-velha">
<div class="linha">
<div class="casa" *ngFor="let quadrado of quadrados, let i = index"
(click)="pegaIndex(i)"
[ngClass]="i == vitoriaArray(i) ? 'casa-vencedor' : null"
(click)="eventoClick($event)" ></div>
</div>
</div>
</div>
I'm using Angular/Cli in version 15, I'm not getting a good understanding of the tests in Angular, thanks for the help.

div.casa is being rendered with an *ngFor and you have to make sure quadrados is a non empty array and not undefined.
Try this:
it('Deve emitir mensagem quando clicar na div', () => {
const emitMessageSpy = spyOn(component.eventoOutput, 'emit')
// Mock quadrados here
component.quadrados = [{ } as any];
// detect the changes here since the view model changed
fixture.detectChanges();
// the div should be there now
let divClick = fixture.debugElement.query(By.css('.casa')).nativeElement
divClick.click()
expect(emitMessageSpy).toHaveBeenCalled
})
Here is a good resource on learning testing with Angular: https://testing-angular.com/.

Related

How do I update component variables in Angular unit tests?

I am having a problem where I set the "headerButtons" and "contractLoaded" component variables in my test but it does not seem to change the values. If I console out or use a debugger in the component as the test runs the variables stay as initially defined (undefined and false).
I have tried lots of different combinations but always the same result.
headerButtons is an #Input
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [HeaderBannerComponent] });
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
...
it('should display the correct amount of header buttons', () => {
const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
expect(debugEl.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
expect(
debugEl.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
Component :
import { Component, Input, OnInit } from '#angular/core';
import { KeyValue } from '#angular/common';
import { ContractEventService } from 'src/app/core/services/contract-event.service';
#Component({
selector: 'app-header-banner',
templateUrl: './header-banner.component.html',
styleUrls: ['./header-banner.component.css'],
})
export class HeaderBannerComponent implements OnInit {
menubar: Map<string, string> = new Map<string, string>();
contractLoaded: boolean = false;
#Input() headerTitle: any;
#Input() headerButtons: string;
constructor(private contractEventService: ContractEventService) {}
ngOnInit(): void {
if (this.headerButtons === 'full') {
this.contractEventService.getContractLoaded().subscribe(
(rs) => {
this.contractLoaded = rs;
this.setButtons();
},
(err) => {
console.warn('failed to get contractLoaded status', err);
}
);
}
this.setButtons();
}
setButtons(): void {
this.menubar = new Map<string, string>();
this.menubar.set('Home', '/iforis/main.do');
if (this.headerButtons === 'full' && this.contractLoaded) {
this.menubar.set('Contacts', '/iforis/s/contacts/');
this.menubar.set('Notes', '/iforis/s/notes/');
}
if (this.headerButtons === 'full' && !this.contractLoaded) {
this.menubar.delete('Contacts');
this.menubar.delete('Notes');
}
this.menubar.set('Exit', '/iforis/exit.do');
}
originalOrder = (
a: KeyValue<string, string>,
b: KeyValue<string, string>
): number => {
return 0;
};
}
Template:
<div class="header-banner box-shadow">
<div class="header-banner__title">
<span id="headerTitleText" class="header-banner__text">
{{ headerTitle }}
</span>
</div>
<div class="header-banner__logo">
<img src="assets/DAFM_Logo_2018.png" />
</div>
<div class="header-banner__btn-container">
<div
*ngFor="
let button of menubar | keyvalue: originalOrder;
let first = first;
let last = last
"
[ngClass]="[
'header-banner__btn',
first ? 'header-banner__btn--first' : '',
last ? 'header-banner__btn--last' : ''
]"
>
<a href="{{ button.value }}" class="header-banner__btn-link">
{{ button.key }}
</a>
</div>
</div>
</div>
That is strange. Can you show the HTML and component typescript as well?
I think I have an idea though about the issue you could be facing.
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
The issue is the debugEl. It becomes stale after you change component variables so you always need a new debugEl after changing variables. I think this is most likely the issue.
====== Edit ======
Maybe we should mock ContractEventService instead of providing the real one.
Try this:
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
let mockContractEventService: jasmine.SpyObj<ContractEventService>;
beforeEach(() => {
// the first string argument is just an identifier and is optional
// the second array of strings are public methods that we need to mock
mockContractEventService = jasmine.createSpyObj<ContractEventService>('ContractEventService', ['getContractLoaded']);
TestBed.configureTestingModule({
declarations: [HeaderBannerComponent],
// provide the mock when the component asks for the real one
providers: [{ provide: ContractEventService, useValue: mockContractService }]
});
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
// formatting is off but return a fake value for getContractLoaded
mockContractEventService.getContractLoaded.and.returnValue(of(true));
// the first fixture.detectChanges() is when ngOnInit is called
fixture.detectChanges();
});
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
// The issue was that we are not calling setButtons
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});

Angular removing elements from a list shared with a service

this problem is driving me crazy.
I have an array defined within a service, which is used in 3 other components:
This is the service, file products.service.ts (notice the product array of Products)
import { Injectable } from '#angular/core';
import { ​​HttpClient } from '#angular/common/http';
import { Product } from './../models/Product';
import { ProductForm, productFormToProduct } from './../models/ProductForm';
// #Injectable({
// providedIn: 'root'
// })
const apiUrl = 'http://localhost:3000/products';
#Injectable()
export class ProductsService {
public products: Product[] = [];
constructor(private http: HttpClient) {}
getProducts() {
return this.http.get(apiUrl)
}
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
return this.http.delete(apiUrl + "/" + p.id)
}
storeNewProduct(pf: ProductForm) {
const idList = this.products.map((x) => {return x.id});
const i = Math.max(...idList) + 1;
const p = productFormToProduct(pf);
p.id = i;
this.products.push(p);
return this.http.post(apiUrl, p)
}
}
This is the component where i subscribe to getProducts, and fill the array (file products.component.ts):
import { Component, OnInit } from '#angular/core';
import { ProductsService } from '../../shared/services/products.service';
import { Product } from '../../shared/models/Product';
#Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
}
}
And this is the component where i subscribe to deleteProduct (file product-card.component.ts):
import { Component, Input, OnInit } from '#angular/core';
import { ProductsService } from '../../services/products.service';
import { Product } from './../../models/Product';
#Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
})
export class ProductCardComponent implements OnInit {
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
}
#Input() product: Product
public buttonDeleteFunction() {
this.productsService.deleteProduct(this.product).subscribe();
}
}
The problem is, when i click on some delete product button, i have this weird behaviour:
Before click:
After click:
Here is the products.component.html file:
<div class="products__header">
<h3 class="products__heading">
Listado de productos ({{ products.length }})
</h3>
<input
class="products__search"
placeholder="Buscador"
type="search"
[(ngModel)]="searchText"
/>
</div>
<p *ngFor="let p of products">{{ p.name }}</p>
<p>{{ products }}</p>
<div class="products__list">
<app-product-card
*ngFor="let p of products | filterNames: searchText"
[product]="p"
></app-product-card>
</div>
Why do i get the expected behaviour in only two of the four places where i use the products list?
I know i can use an Output to manually remove the item from the list when i click the button, but i have been told that services are used instead of Inputs/Outputs when i want to share between multiple components, so i'd rather not use an Output for this
When you use your approach with common data on service layer then a common pitfall is that Angular does not detect the changes that affect your component. In that case you must inform your component for those changes using an emmiter.
Use an emmiter on service
productUpdated :EventEmitter = new EventEmitter();
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
this.productUpdated.emit(this.products);
return this.http.delete(apiUrl + "/" + p.id)
}
And then listen for that change ProductsComponent
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
this.productsService.productUpdated.subscribe( (data) => {
this.products = data;
});
}

Binding input value in unit test

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?

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();
}));
});
});

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