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.
Related
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();
});
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
Recently ive made an angular 2 todo app that is working, however im not using a service for this app, and ive heard that using a service is the way to go. But i am not entirely sure how i refactor my code so that i can push data into my service instead.
My component:
import { Component } from '#angular/core';
import { Todo } from './todo';
import { TODOS } from './mock-todos';
import { TodoService } from './todo.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass'],
providers: [TodoService]
})
export class AppComponent {
title = 'Todo List';
selectedTodo: Todo;
completed = false;
constructor(private todoService: TodoService){
}
onSelect(todo: Todo): void {
this.selectedTodo = todo;
}
addTodo(value: any) {
this.todoService.addTodo(value);
console.log(value);
}
deleteTodo(todo) {
this.todos.splice(todo,1);
console.log("This todo has been deleted"+ todo);
}
completedTodo(todo){
todo.isCompleted = !todo.isCompleted;
todo.completed = !todo.completed;
}
}
My Service:
import { Injectable } from '#angular/core';
import { Todo } from './todo';
#Injectable()
export class TodoService {
todos: Todo[] = [];
lastId: number = 0;
constructor() { }
addTodo(value: any) {
this.todos.push(value);
console.log("This was pushed");
}
}
I thought i was able to use the service to push my data there , instead of having the component to handle this. So the service can be used for other components.
I would be happy to get a reply to this.
Instead of performing actions on variable in component, you can instead store your todos in the service, and when you want to make changes to your array, you just call the service functions. This is pretty well covered in the Services tutorial in the official docs, but just to throw in a short example for getting and adding todos:
In component, get the todos in OnInit and store in local variable.
ngOnInit() {
this.todos = this.todoService.getTodos()
}
The adding of a todo, call the service to do the adding.
addTodo(todo) {
this.todoService.addTodo(todo)
}
Your TodoService looks codewise totally right, so you were almost all there with your code :)
I would like to have custom errors in my Angular2 app. Thus I have extended ErrorHandler in my component:
import { Component, ErrorHandler, OnInit } from '#angular/core';
import { GenericError } from './generic-error.component';
#Component({
selector: 'custom-error-handler',
templateUrl: 'app/error-handler/custom-error-handler.component.html?' + +new Date()
})
export class CustomErrorHandler extends ErrorHandler {
errorText: string;
constructor() {
super(false);
}
ngOnInit() {
this.errorText = 'Initial text!';
}
public handleError(error: any): void {
if (error.originalError instanceof GenericError) {
console.info('This is printed to console!');
this.errorText = "I want it to print this in the template!";
}
else {
super.handleError(error);
}
}
}
My template simply contains:
<span style="color:red">{{errorText}}</span>
First I see "Initial text!" in the template as set in ngOnInit. That's as expected.
I can then throw a new exception like this from a different component:
throw new GenericError();
and it hits the code with handleError and prints to console but it doesn't update my template errorText with:
"I want it to print this in the template!"
It's like it ignores my template, when inside the handleError function.
What could be the problem here?
* ADDED MORE INFORMATION *
I thought I should add some more information. So here is the module I made for CustomErrorHandler (maybe the problem is with the providers?):
import { NgModule, ErrorHandler } from '#angular/core';
import { CommonModule } from '#angular/common';
import { CustomErrorHandler } from './custom-error-handler.component';
#NgModule({
declarations: [
CustomErrorHandler
],
imports: [
CommonModule
],
exports: [
CustomErrorHandler
],
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler }
]
})
export class CustomErrorModule { }
There is indeed only one instance of the CustomErrorHandler (I checked with the Augury Chrome plugin).
For completeness, here is is the GenericError component:
export class GenericError {
toString() {
return "Here is a generic error message";
}
}
The solution was to add a service as suggested in the question's comment track. This way I can set the property in the component and eventually show it in the template.
I created the service, so that it has a function which takes one parameter. Injected the service, call the service's function from the handleError in the component function, and send the text I want in the template as the parameter. Then I use an observable, to get the text back to the component.
In the constructor of the component, I added this observer.
let whatever = this.cs.nameChange.subscribe((value) => {
setTimeout(() => this.errorText = value);
});
I needed to add the setTimeout, or else it would not update the template before the second time the observable was changed.
Phew! The Angular team should make this global exception handling easier in future releases.
I am having a hard time using a async object in a html composition.
Here is my model:
export class Version {
isGood: boolean;
constructor(isGood: boolean) {
this.isGood= isGood;
}
}
This model is called by a component as follows:
#Injectable()
export class MyComponent {
public version: Version;
constructor(private _myService: VersionService) {}
getVersion(): void {
// async service that gets the versions
this._myService.getVersion().subscribe(
data => this.version= data,
error=> console.log(error),
() => console.log("getting all items complete")
);
}
}
My template references to the version variable as follows:
<button (click)="getVersion()">Get Version</button>
<hr>
<p style="color:red">{{error}}</p>
<h1>Version</h1>
<p>{{version.isGood}}</p>
However, I get an exception:
Cannot read property 'isGood' of undefined
From scavenging the internet, I see that my problem is because the version object is null. If I do something like:
<p>{{version | json}}</p>
I can see the correct version
If I do something like
<p>{{version.isGood | async}}</p>
I see nothing
If I edit MyComponent, and set
public version: Version = new Version();
I can execute the .isGood property fetch, but it is always empty.
Is there a different way I am supposed to load a property if I am using it in an asynchronous manner?
Use the ? operator or use an *ngIf.
<p>{{version?.isGood}}</p>
<p *ngIf="version">{{version.isGood}}</p>
Try this:
<p>{{version?.isGood}}</p>
This tells Angular to protect against version.isGood being undefined or null until you click and fetch the data for version through your service.
First me correct you. #Injectable() makes a normal typescript class as injectable service where you can share data.
To make a component you need to use #Component decoratore.
The process of data sharing between component and within the application is to create a service and add that as provides in module. And then its singleton object will available everyshere.
//module
import {NgModule} from '#angular/core';
import {YourService} from "./services/your-service";
#NgModule({
imports: [
BrowserModule
],
declarations: [
AppComponent
],
providers: [
YouService
],
bootstrap: [AppComponent]
})
export class AppModule {
}
//this is your component
import {Component} from '#angular/core';
import {YourService} from "../../services/your-service";
#Component({
selector: 'component-app',
templateUrl: '../../views/app.component.html',
})
export class HeaderComponent {
constructor(public yourService: YourService) {
}
}
//your service
import {Injectable} from "#angular/core";
#Injectable()
export class YourService {
private _message: string = 'initial message';
private _style: string = 'success';
get message(): string {
return this._message;
}
set message(value: string) {
this._message += value;
}
get style(): string {
return this._style;
}
set style(value: string) {
this._style = value;
}
}
//finally your view
<div class="row">
<div [class]=""><h1>{{swapService.message}}</h1></div>
</div>
Observable Data services.
#Injectable()
export class MyComponent {
public version = new ReplaySubject<Version>();
constructor(private _myService: VersionService) {}
init(): void {
// async service that gets the versions
this._myService.getVersion().subscribe(
data => this.version.next(data),
error=> console.log(error),
() => console.log("getting all items complete")
);
}
getVersion(): void {
this.version.asObservable();
}
}
In the template
<button (click)="init()">Get Version</button>
<hr>
<p style="color:red">{{error}}</p>
<h1>Version</h1>
<p>{{(version |async)?.isGood}}</p>