Jasmine: testing observables with events - javascript

I am trying to test an angular2 application. I have a login form, which uses an observable to send data to the backend:
doLogin() {
this.usersService.login(this.model)
.subscribe((data) => {
console.log("In observable: " + data.isSuccess);
if (!data.isSuccess) {
this.alerts.push({});
}
});
}
In tests I am adding a spy on the service function, which returns observable, so that component can work on it:
usersService.login.and.returnValue(Observable.of(
<LoginResponse>{
isSuccess: true
}));
When everything is ready, I dispatch an event on submit button, which triggers doLogin function in component:
submitButton.dispatchEvent(new Event("click"));
fixture.detectChanges();
It works correctly. Unfortunately, when I check if usersService.login has been called in the test:
expect(usersService.login).toHaveBeenCalled();
I get an error, because the observable didn't finish and login has not been called yet.
How should I make sure, I check my spy after observable has finished?

I don't know how you configure the service on the component but it works for me when I override providers of the component created from TestComponentBuilder.
Let's take a sample. I have a service that returns a list of string:
import {Observable} from 'rxjs/Rx';
export class MyService {
getDogs() {
return Observable.of([ 's1', 's2', ... ]);
}
}
A component uses this service to display a list asynchronously when clicking a button:
#Component({
selector: 'my-list',
providers: [MyService],
template: `
<ul><li *ngFor="#item of items">{{ item }}</li></ul>
<div id="test" (click)="test()">Test</div>
`
})
export class MyList implements OnInit {
items:Array<string>;
service:MyService;
constructor(private service:MyService) {
}
test() {
this.service.getDogs().subscribe(
(dogs) => {
this.items = dogs;
});
}
}
I want to test that when I click on the "Test" button, the test method of the component is called and the getDogs method of the service is indirectly called.
For this, I create a test that instantiate directly the service and load the component using TestComponentBuilder. In this case, I need to call the overrideProviders method on it before calling createAsync. This way, you will be able to provide your spied service to be notified of the call. Here is a sample:
let service:MyService = new MyService();
beforeEach(() => {
spyOn(service, 'getDogs').and.returnValue(Observable.of(
['dog1', 'dog2', 'dog3']));
});
it('should test get dogs', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.overrideProviders(MyList, [provide(MyService, { useValue: service })])
.createAsync(MyList).then((componentFixture: ComponentFixture) => {
const element = componentFixture.nativeElement;
componentFixture.detectChanges();
var clickButton = document.getElementById('test');
clickButton.dispatchEvent(new Event("click"));
expect(service.getDogs).toHaveBeenCalled();
});
}));
Edit
Since the event is triggered asynchronously, you could consider to use fakeAsync. The latter allows you to completly control when asynchronous processing are handled and turn asynchronous things in to synchronous ones.
You could wrap your test processing into
fakeAsync((): void => {
var clickButton = document.getElementById('test');
clickButton.dispatchEvent(new Event("click"));
expect(service.getDogs).toHaveBeenCalled();
});
For more details, you could have a look at this question:
Does fakeAsync guarantee promise completion after tick/flushMicroservice

Related

Javascript / Angular - html displays before rendering code

I have a function to get rates from products, so lets say I have one product with two rates. So my product has two rates. Then, when I get those rates I must get the prices attached to my product. So for each rate I have to look for its prices.
The next code below explains this:
this.loadProductInfo = true; // bool to load data in my form
// First of all, I get rates from API
// const rates = this._http....
// Now, for each rate I must search If my product/products have a price:
this.rates.forEach((rate, index, arr) => {
this._glbGetPricesForProduct.getPrice(params).subscribe(response => {
if (!arr[index + 1]) {
this.initForm();
this.loadProductInfo = false;
}
})
});
The variable loadProductInfo it loads content in my form, so in my html I have:
<form *ngIf="!loadProductInfo"></form>
But form it still give me error: could not find control name.
But if I do this instead, it works correctlly:
setTimeout(() => {
this.initForm();
this.loadProductInfo = false;
}, 2000);
So what I want its to say my form to wait until I have all code loaded and then after it, load its contents. But instead it cant find the control because it loads before code. Any help I really appreciate it.
The main mistake I see there is that you are looping over async data which may not be there when your code execute the for each loop (your rates).
I would build an observable with your rates as a source:
...
$rates: Observable<any> = this._http.get(...);
rates.pipe(
mergeMap((rates) => {
const priceByRates: Observable<any>[] = rates.map((rate, index, arr) => this._glbGetPricesForProduct.getPrice(params));
return combineLatest(pricesByRates); // if getPrice complete right away, use forkJoin() instead
})
).subscribe(res => {
// No need to check for the last item, all rates have been checked for possible price
this.initForm();
this.loadProductInfo = false;
});
...
This implementation should wait for your api calls to resolve before printing your form.
Since you are hiding the entire form, it may be better to just move the API call into a resolver so that the page does not render until the data is ready.
Here is a minimal StackBlitz showcasing this behavior: https://stackblitz.com/edit/angular-ivy-4beuww
Component
In your component, include an ActivatedRoute parameter via DI.
#Component(/*omitted for brevity*/)
export class MyComponent {
constructor(private route: ActivatedRoute) {
// note: 'data' is whatever you label your resolver prop in your routing setup
route.data.subscribe(resolved => {
if ("data" in resolved) this.resolveData = resolved["data"];
});
}
}
Route Setup
And in your router setup you would have the following:
const routes: Routes = [
{
path: 'my-route-path',
component: MyComponent,
resolve: {
data: MyResolver
}
}
];
Resolver
Finally, your resolver would make your API call utilizing your service:
#Injectable({providedIn: 'root'})
export class MyResolver() implements Resolve<T> {
constructor(private service: MyService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | any {
return this.service.myRequest();
}
}
The final result will be that your view will not be rendered until your data is ready.

Subscribing to a change, but needing to see variable from where Service is used?

My code has been refactored and some extracted into a service that subscribes to functions. However, my original code had a call within the subscription that referenced a variable within the file, but now I'm not sure how to best reach it?
I am struggling with where to place the line:
this.select.reset('some string'); found within the subscribeToMessageService() function.
Original code
event.component.ts
select: FormControl;
#ViewChild('mySelect') mySelect: ElementRef;
subscribeToMessageService() {
this.messageService.serviceMsg
.subscribe(res => {
// unrelated code
this.select.reset('some string');
});
}
subscribeToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.select = new FormControl(res.status);
this.select.valueChanges.subscribe(value => {
// manual blurring required to stop error being thrown when popup appears
this.selector.nativeElement.blur();
// do something else
});
});
}
Refactored code
status.service.ts
subscribeToMessageService(): void {
this.messageService.serviceMsg
.subscribe(res => {
// unrelated code
// This is where 'this.select.reset('some string');' would have gone
});
}
status.component.ts
select: FormControl;
#ViewChild('exceptionalSelect') selector: ElementRef;
subscribeToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.select = new FormControl(res.status);
this.select.valueChanges.subscribe(value => {
// manual blurring required to stop error being thrown when popup appears
this.selector.nativeElement.blur();
this.onStatusChange(value);
});
});
}
Since you still want to subscribe to the original source messageService.serviceMsg your new StatusService needs to expose this observable to the injecting component (StatusComponent).
This can be done for example by creating a public observable in the StatusService (possibly by utilising rxjs Subject or angular EventEmitter) and triggering the emit in the subscription of messageService.serviceMsg.
Then your StatusComponent only needs to inject StatusService and do
this.statusService.serviceMsg // <-- might choose another name to make clear that this is passed on.
.subscribe(res => {
// unrelated code
this.select.reset('some string');
});

Angular - Unit testing Subject()?

I have a Angular service that simply uses a Subject, but I am unsure of how to write unit tests for it.
I saw [this thread][1], but I didn't find it terribly helpful.
I have attempted to mock next() but I am more than a little lost.
You should spy on service.serviceMsg and not service, because next() method appears to be on serviceMsg subject.
it('should catch what is emitted', () => {
const nextSpy = spyOn(service.serviceMsg, 'next');
service.confirm(ACTION);
expect(nextSpy).toHaveBeenCalled();
});
EDIT :
You should also change the way you are creating service instance. What you show in your code is applicable only for component instance creation
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
service = TestBed.get(MessageService); // get service instance
httpMock = TestBed.get(HttpTestingController);
});
Firstly you can just subscribe to your Subject and inside expect some value and after that just execute method which will emit that:
it('should catch what is emitted', () => {
service.serviceMsg.subscribe(msg => {
expect(msg).toEqual(something);
});
service.confirm(value); // this should match what you expect above
});

Executing code after router.navigate in Angular2

I wonder if there is a way to execute something after i navigate to a different "view" using angular router.
this.router.navigate(["/search", "1", ""]);
// Everything after navigate does not not get executed.
this.sideFiltersService.discoverFilter(category);
this.router.navigate returns a promise so you can simply use:
this.router.navigate(["/search", "1", ""]).then(()=>{
// do whatever you need after navigation succeeds
});
// In javascript
this.router.navigate(["/search", "1", ""])
.then(succeeded => {
if(succeeded)
{
// Do your stuff
}
else
{
// Do some other stuff
}
})
.catch(error => {
// Handle the error
});
// In typescript you can use the javascript example as well.
// But you can also do:
try
{
let succeeded = await this.router.navigate(["/search", "1", ""]);
if(succeeded)
{
// Do your stuff
}
else
{
// Do some other stuff
}
}
catch(error)
{
// Handle the error
}
Not entirely sure of the context but an option would be to subscribe to a change in the URL using ActivatedRoute
https://angular.io/docs/ts/latest/guide/router.html#!#activated-route
Here's an example:
...
import { ActivatedRoute } from '#angular/router';
...
private _routerSubscription: any;
// Some class or service
constructor(private _route: ActivatedRoute){
this._routerSubscription = this._route.url.subscribe(url => {
// Your action/function will go here
});
}
There are many other observables you can subscribe to in ActivatedRoute which are listed in that link if url isn't quite what you need.
The subscription can be done in the constructor() or in an ngOnInit() depending on what suits you best, just remember to clean up after yourself and unsubscribe in an ngOnDestroy() :)
this._routerSubscription.unsubscribe();
If you are navigated from ComponentA to ComponentB then after navigating you can do any actions in ngOnInit() function of ComponentB, depending upon the parameters passed in the route.
You also have to ensure that there are no ongoing subscriptions... I faced the same problem and in my case there was a subscription which changed route. So the route has been changed twice. But practically you can use promises, thats right

Angular2 + Angular1 + ES5 syntax component subscription

First of all let me say I am still using ES5, mostly because I am writing this for a frontend of a Google Apps Scripts application and didn't have the time/patience to make TypeScript work.
I am currently using this method in order to upgrade my Angular1 app to Angular2:
http://www.codelord.net/2016/01/07/adding-the-first-angular-2-service-to-your-angular-1-app/
I have a overlayLoaderService service to show a loading spinner in an overlay div with simple functions to get and set the loading state, and a overlayLoader component to show the div itself.
Service:
var overlayLoaderService = ng.core.
Injectable().
Class({
constructor: function() {
this.loading = false;
this.stateChange = new ng.core.EventEmitter();
},
setState: function(state) {
this.loading.value = state;
this.stateChange.emit(state);
},
getState: function() {
console.log(this.loading);
}
});
upgradeAdapter.addProvider(overlayLoaderService);
angular.module('Gojira').factory('overlayLoaderService', upgradeAdapter.downgradeNg2Provider(overlayLoaderService));
Component:
var OverlayLoaderComponent = ng.core
.Component({
selector: 'overlay-loader',
template: '<div [hidden]="loading" id="overlay-loader"></div>',
providers: [overlayLoaderService]
}).Class({
constructor: [overlayLoaderService, function(overlayLoaderService) {
this.loading = !overlayLoaderService.loading.value;
this._subscription = overlayLoaderService.stateChange.subscribe(function (value) {
console.log(value);
this.loading = !value;
});
}],
});
angular.module('Gojira').directive('overlayLoader', upgradeAdapter.downgradeNg2Component(OverlayLoaderComponent));
What I am trying to do is to achieve is the component to update its loading property when I call setState() method in the overlayLoaderService.
The subscription is never called, so I guess I am doing something terribly wrong here.
Any help would be appreciated.
remove
providers: [overlayLoaderService]
from OverlayLoaderComponent should work, as the provider has already been added to the adapter. Otherwise, it seems like angular 1 component and angular 2 component are using different instance of that service.

Categories