I am testing an angular app and especially this HTML input:
<form name="editForm" role="form" novalidate (ngSubmit)="save()" #editForm="ngForm">
<input type="text" name="nombre" id="field_nombre"
[(ngModel)]="paciente.nombre" required/>
(etc. f.e. button on submit...)
Here is my component:
imports....
export class PacienteDialogComponent implements OnInit {
paciente: Paciente;
....
save() {
this.isSaving = true;
if (this.paciente.id !== undefined) {
this.subscribeToSaveResponse(
this.pacienteService.update(this.paciente));
} else {
this.subscribeToSaveResponse(
this.pacienteService.create(this.paciente));
}
}
}
Here is my patient.model.ts
export class Paciente implements BaseEntity {
constructor(
public id?: number,
public nombre?: string,
public sexo?: Sexo,
.....
I want to test the form which means that on submit it is really calling teh save() function.
I have this in my test:
describe('Paciente Management Dialog Component', () => {
let comp: PacienteDialogComponent;
let fixture: ComponentFixture<PacienteDialogComponent>;
let debugElement: DebugElement; //create a debgElement for testing
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [OncosupTestModule,
OncosupSharedModule,
BrowserModule,
FormsModule,
],
declarations:...
],
providers: [
...
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PacienteDialogComponent);
comp = fixture.componentInstance;
debugElement = fixture.debugElement;
});
//a default generated test which controls if the save method really saves a new patient with its name, id, sex, etc.
it('Should call create service on save for new entity',
inject([],
fakeAsync(() => {
// GIVEN
const entity = new Paciente();
spyOn(service, 'create').and.returnValue(Observable.of(new HttpResponse({body: entity})));
comp.paciente = entity;
// WHEN
comp.save();
tick(); // simulate async
// THEN
expect(service.create).toHaveBeenCalledWith(entity);
expect(comp.isSaving).toEqual(false);
expect(mockEventManager.broadcastSpy).toHaveBeenCalledWith({ name: 'pacienteListModification', content: 'OK'});
expect(mockActiveModal.dismissSpy).toHaveBeenCalled();
})
)
);
// And teh second thing I want to test is if ngSubmit is really calling the save() function
it ('should call the onSubmit method', async(() => {
//fixture.detectChanges();
spyOn(comp,'save');
var1 = debugElement.query(By.css('button')).nativeElement;
console.log('print button ' + var1);
var1.click();
expect(comp.save).toHaveBeenCalledTimes(0);//verify...
}));
//And also if isSaving is set to true
it ('should set isSaving to true', async(() => {
comp.save();
expect(comp.isSaving).toBeTruthy();
}));
1.Now I have these questions: The first test is generated by default and not written by me. In this line const entity = new Paciente(); should I call parameters of Paciente? Like id, sex, name or leave it like this by default without parameters. Th epurpose of this first test if to check if really the save() function saves a patient and his data like id, sex, etc.
2.For the second test I read it in a tutorial of angular that: HaveBennCalled(0) is the right thing to test if this spy is called and how many times. But anyway does it really tests if the button calls the function save(). I think it only checks if thebutton haven´t been called before, but not if it is callled right now in save function.
3.And are these 3 tests enough and complete for a form submitting?
Following my comments, here is how to test if a form is submitted correctly.
Let's say you have an interface Patient :
export interface Patient {
id: number;
name: string;
}
In your component, you have a form, and you submit it through submit() :
submit() {
this.patientService.savePatient(this.patient).subscribe(result => {
console.log('Patient created');
});
}
Now your service make the HTTP call and checks if the fields are okay :
savePatient(patient: Patient): Observable<any> {
if (typeof patient.id !== number) { return Observable.throw('ID is not a number'); }
if (typeof patient.name !== string) { return Observable.throw('Name is not a string'); }
return this.http.post<any>(this.url, patient);
}
Then your tests should look like this. First, the component :
it('Should call the service to save the patient in DB', () => {
// Spy on service call
// Expect spy to have been called
});
it('Should log a message on success', () => {
// Spy on console log
// Expect console log to have been called with a string
});
You can also test if the error is treated correctly, if you have error codes, etc.
Now in the service :
it('Should throw an error if the ID is not a number', () => {
// Mock a patient with a string ID
// Expect an error to be thrown
});
// Same thing for the name, you get the idea
it('Should make an HTTP call with a valid patient', () => {
// Spy on the HttpTestingController
// Expect the correct endpoint to have been called, with the patient as the payload
});
The general idea of those tests is to cover any case that could happen. This will allow you to prevent side effects : for instance, if one day you decide to pass your ID to string, the unit test will fail and tell you
You expect me to send a string but I pass only with a number
This is the purpose of a unit test.
Related
Objective:
I'm working with a service. The objective is to create a service that will serve as a creation for confirmation dialog and will return whether confirm or cancel was clicked in the popup.
Code:
product.component.html
<button class="edit-button" (click)="confirmFeatureProductDialog(data)">Confirm</button>
product.component.ts
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
popupDialog.service.ts
import { Injectable } from "#angular/core";
import { Observable, Subject } from "rxjs";
import Swal from 'sweetalert2'
Injectable();
export class PopupDialogService {
public mySubject = new Subject<boolean>();
confirmationPopup(textDetail?): Observable<any> {
Swal.fire({
title: 'Are you sure?',
text: textDetail,
icon: 'success',
showConfirmButton: true,
showCancelButton: true,
}).then((result) => {
if (result.value) {
this.mySubject.next(true);
return;
}
Swal.close();
});
return this.mySubject.asObservable();
}
}
Result & Issue:
When I run this the first time everything seems to be working fine.
When I click on the button the 2nd time after clicking either cancel or confirm the response from the product.component.ts is duplicated twice i.e the console.log from the subscribe response is repeated twice and console.log shows "Running" "Running".
When I click the button third time (once the dialog closes after clicking confirm/cancel) the response is repeated thrice and console shows "Running" 3 times.
Things I've tried so far:
Add the service to app.module.ts provider instead of product.module.ts providers. Doesn't change anything.
Checked if the service's constructor is called multiple times on 2nd and onwards execution. It doesn't execute multiple times. Only the subscription response is executed multiple times.
Summary:
Running subscription on a function returning observable from service. First time the response of the subscription is okay however when the function is re-opened the 2nd time it throws the response twice, after closing and re-running the function gives the response thrice and so on.
You are subscribing to the same subject multiple times without unsubscribing from the previous subscription.
Subscribe to the subject on init and remove the asobservable return from the service
// component code
ngOnInit() {
let result;
this._popupDialogService.mySubject.subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
this._popupDialogService.confirmationPopup(message)
}
// Service code
confirmationPopup(textDetail?) {
Swal.fire({
title: 'Are you sure?',
text: textDetail,
icon: 'success',
showConfirmButton: true,
showCancelButton: true,
}).then((result) => {
if (result.value) {
console.log(result)
this.mySubject.next(true);
return;
}
Swal.close();
});
}
It cause by every time you clicked, You have create new Subscription to mySubject on PopupDialogService
Solution: You need to move subscription part to ngOnInit of the component to prevent the subscription duplicated.
.component.ts
export class AppComponent implements OnInit, OnDestroy {
sub: Subscription;
constructor(private _popupDialogService: PopupDialogService) {}
ngOnInit() {
this.sub = this._popupDialogService.mySubject.subscribe((res) => {
console.log('Running');
// result = res;
// if (result) {
// this.featureProduct(product._id);
// }
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
confirmFeatureProductDialog() {
let message = 'Do you want to set this product?';
let result;
this._popupDialogService.confirmationPopup(message);
}
}
Here is example: stackblitz
this issue is occurs because of observer of this._popupDialogService.confirmationPopup(message)
you need to declare in product component for subscription:
subscription!: Subscription
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this.subscription =this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
in ondestory lif cycle hook just unsubscribe it.
ngOnDestroy() {
this.subscription.unsubscribe()
}
try to change your method like:
sub: Subscription;
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this.sub = this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
this.subs.unsubscribe();
}
);
}
That it could fix your issue, but I think in your case you need a different approach to the problem. Maybe it's better using a service like that:
export class DialogService {
dialogConfig: EventEmitter<DialogConfig> = new EventEmitter(null);
afterClosed: EventEmitter<DialogResponse> = new EventEmitter(null);
constructor() { }
/** OPEN DIALOG DEFINED IN APP.COMPONENT.HTML
*
* #title define the optional title of a dialog (title to translate with pipe translate)
* #action action define the action component to use
* #message define the message of a dialog (message to translate with pipe translate)
*
*/
open(title: string, action: string, message?: string, params?: Object): EventEmitter<DialogResponse> {
this.afterClosed = new EventEmitter(null);
this.dialogConfig.emit({
visibile: true,
title,
action,
message,
params
});
return this.afterClosed;
}
/** CLOSE DIALOG DEFINED IN APP.COMPONENT.HTML */
close(res: boolean, note?: string) {
this.dialogConfig.emit({ visibile: false });
note ? this.afterClosed.emit({ response: res, note: note }) : this.afterClosed.emit({ response: res });
}
}
used in your components like this for open:
this.dialog.open("title", "action", "message").subscribe(res => {
...stuffs
})
and for close:
this.dialog.close(true | false)
I am solving this issue:
The application flow:
I have to call the first API endpoint (let's call it EP-A for simplicity) which takes Blob as body and fileType as a request parameter. Its performed via calling automatically generated class
uploadFile$Response(params?: {
fileType?: 'USER_AVATAR' | 'UNKNOWN' | 'DELIVERY_LOGO' | 'PAYMENT_LOGO' | 'ITEM_PICTURE';
body?: { 'file'?: Blob }
}): Observable<StrictHttpResponse<FileUploadResponse>> {
const rb = new RequestBuilder(this.rootUrl, FileControllerService.UploadFilePath, 'post');
if (params) {
rb.query('fileType', params.fileType, {});
rb.body(params.body, 'application/json');
}
return this.http.request(rb.build({
responseType: 'blob',
accept: '*/*'
})).pipe(
filter((r: any) => r instanceof HttpResponse),
map((r: HttpResponse<any>) => {
return r as StrictHttpResponse<FileUploadResponse>;
})
);
}
The StrictHttpResponse<T> is simply an interface holding a "generic" body (so you can retrieve data that will have a structure defined by swagger from which this method is generated).
Then the result FileUploadResponse which is an object like
{
uuid: string,
time: Timestamp
...
Other members omitted for simplicity
...
}
is sent to another EP (let's call it EP-B) right after EP-A call returns a value, EP-B takes an object below as a body and currently logged person as a path variable.
{
uuid: string
}
So before calling EP-B the result from EP-A should be parsed (in this case, the uuid field should be taken and put into a new object for EP-B calling)
Again via the generated method with a similar signature as the one above (and I will omit it for simplicity).
If everything performed well, I´d like to let the caller know about that. If anything failed (any of these 2 EP calls), I´d like to let it know to call of this method to react somehow (show alert, change page somehow, ...)
The method I have is now incomplete, I do not know how to "connect" these 2 Observables, I´ve read about mergeMap, flatMap, etc. but I am not sure how to use it in my case.
updateUserAvatar(avatar: Blob): Observable<boolean> {
return new Observable<boolean>((observer) => {
// Calling EP-A
this.avatarFormChangeRequestSubscription = this.fileControllerService.uploadFile$Response({
fileType: 'USER_AVATAR',
body: {
file: avatar
}
})
.subscribe((response: StrictHttpResponse<FileUploadResponse>) => {
// Handle returned UUID and somehow pass it into an observable belog
console.log(response);
},
(error: any) => {
observer.error(error);
});
// Creating object for EP-B calling
const avatarUpdateParams = {
id: 1, // Just dummy ID for now, will be dynamically changed
body: {
avatarUUID: '' // the UUID from observable response above should be placed here
}
};
// Calling EP-B
this.avatarFormChangeRequestSubscription = this.userControllerService.updateUserAvatar$Response(avatarUpdateParams)
.subscribe((response: StrictHttpResponse<string>) => {
// Handle successfull avatar upload (change the "Logged user" object avatar to change it everywhere etc
console.log(response);
observer.next(true);
},
(error: any) => {
observer.error(error);
});
});
}
At the end I would like to add "use case" flow too to understand what I am trying to achieve from user view:
User uploads his photo which is firstly uploaded into a file system (and linked with database record) on BE side, then this file is linked to his profile as his profile picture.
You could do it using rxjs. Something like that might works :
this.fileControllerService.uploadFile$Response({
fileType: 'USER_AVATAR',
body: {
file: avatar,
},
})
.pipe(
tap((responseOfFirstApiCall: StrictHttpResponse<FileUploadResponse>) => {
// Do whatever you want here, but you might not need that since you get the response below as well (in the flatMap)
// Handle returned UUID and somehow pass it into an observable belog
console.log(response);
}),
flatMap(
(responseOfFirstApiCall: StrictHttpResponse<FileUploadResponse>) => {
// Creating object for EP-B calling
const avatarUpdateParams = {
id: 1, // Just dummy ID for now, will be dynamically changed
body: {
avatarUUID: '', // the UUID from observable response above should be placed here
},
};
return this.userControllerService.updateUserAvatar$Response(avatarUpdateParams);
}
),
tap((responseOfTheSecondApiCall: StrictHttpResponse<string>) => {
// Handle successfull avatar upload (change the "Logged user" object avatar to change it everywhere etc
console.log(response);
observer.next(true);
}),
catchError((err: any) => of(err))
)
.subscribe(); // Empty subscribe() call to trigger the http request. Not needed if you get the result somewhere else (eg if your method return an observable that you want to handle the result somewhere else)
flatMap() is the same as mergeMap. Change it as you wish, there's a lot of option like map or switchMap that you should learn about since they are useful.
Basically, the pipe allow you to chain functions, and if there is an error, then the catchError is triggered.
Tip: Note that what is in the pipe is executed BEFORE the result of your api call. So if you want to do something with your result before to get it, then think about rxjs:
service
getUser(id: string) {
return this._http.get<any>(url).pipe(
map(result => result.email), // Return only the email
);
}
component:
ngUnsubscribe = new Subject();
ngOnInit() {
this._userService.getUser(1)
.pipe(takeUntil(this.ngUnsubscribe)) // Don't forget to unsubscribe !
.subscribe(email => console.log('email = ', email))
}
ngOnDestroy() {
this.ngUnsubscribe.unsubscribe();
// or
// this.ngUnsubscribe.next();
// this.ngUnsubscribe.complete();
}
I am trying to return the document id when I create it. Since Firebase functions are async the return value is not completed until the query is done. How can I prevent this so I can wait to get the value when the query is done?
This function create the document is located in a service:
public makeDoc(title: string, score: number): any{
const fields = {
title: title,
score: number
}
this.db.collection('saved').add(fields)
.then(function(ref) {
console.log(ref.id);
return ref.id;
})
}
I call this from a function which is located in a component:
onCreate() {
const str = this.createService.makeDoc(this.title, this.score);
console.log(str);
}
Try following:
const fields = {
title: title,
score: number
}
var newFieldsRef = this.db.collection('saved').push();
this.db.collection('saved').child(newFieldsRef).set(fields);
var id = newFieldsRef.toString();
You don't want to prevent waiting until the query is done, you should embrace the use of promises here.
First, if you haven't, make sure you import the firestore namespace in the service:
import { firestore } from 'firebase';
Now, for your service:
I had to slightly change your makeDoc method as the fields object wasn't being created in a valid way (e.g. reusing the number type):
public makeDoc(titleParam: string, scoreParam: number): Promise<firestore.DocumentReference> {
const fields = {
title: titleParam,
score: scoreParam
};
return this.db.collection('saved').add(fields);
}
This now returns a Promise<DocumentReference> which, when resolved, the reference will point to the created document.
Now, the call to it in onCreate looks like:
onCreate() {
this.createService.makeDoc('myTitle', 123)
.then((ref) => { console.log(ref.id); })
.catch((err) => { console.log(err); });
}
And this will log the id as soon as it is available.
I am trying to implement some unit tests on a form to see if the validation rules are working as expected.
from this page : https://github.com/aurelia/testing/issues/63
I found this implementation : https://github.com/aurelia/validation/blob/master/test/validate-binding-behavior.ts
and I tried to implement it in my project
login.spec.js
import {bootstrap} from 'aurelia-bootstrapper';
import {StageComponent} from 'aurelia-testing';
import {PLATFORM} from 'aurelia-pal';
import { configure, blur, change } from './shared';
import { Login } from './login';
describe('ValidateBindingBehavior', () => {
it('sets validateTrigger', (done) => {
const component = StageComponent
.withResources(PLATFORM.moduleName('features/account/login/login'))
.inView('<login></login>')
.boundTo({});
component.bootstrap(configure);
let viewModel;
const renderer = { render: jasmine.createSpy() };
component.create(bootstrap)
// grab some references.
.then(() => {
viewModel = component.viewModel;
viewModel.controller.addRenderer(renderer);
})
.then(() => expect(viewModel.controller.errors.length).toBe(0))
.then(() => blur(viewModel.firstName))
.then(() => expect(viewModel.controller.errors.length).toBe(1))
.then(() => component.dispose())
.then(done);
});
});
login.js
import { inject, NewInstance } from 'aurelia-dependency-injection';
import { ValidationController } from 'aurelia-validation';
import { User } from './login.model';
#inject(NewInstance.of(ValidationController), User)
export class Login {
constructor(controller, user) {
this.controller = controller;
this.firstName = '';
this.lastName = '';
this.userName = '';
this.showForm = true;
this.user = user;
}
};
login.model.js
import {ValidationRules} from 'aurelia-validation';
export class User {
firstName = '';
lastName = '';
userName = '';
constructor() {
ValidationRules
.ensure('firstName')
.required()
.ensure('lastName')
.required()
.minLength(10)
.ensure('userName')
.required()
.on(this);
}
}
shared.js
import {DOM, PLATFORM} from 'aurelia-pal';
export function configure(aurelia) {
return aurelia.use
.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-validation'))
}
export function blur(element) {
element.dispatchEvent(DOM.createCustomEvent('blur', {}));
return new Promise(resolve => setTimeout(resolve));
}
export function change(element, value) {
element.value = value;
element.dispatchEvent(DOM.createCustomEvent('change', { bubbles: true }));
return new Promise(resolve => setTimeout(resolve));
}
and here is a piece of html markup :
<div>
<input ref="firstName" type="text" value.bind="user.firstName & validateOnBlur"
validation-errors.bind="firstNameErrors">
<label style="display: block;color:red" repeat.for="errorInfo of firstNameErrors">
${errorInfo.error.message}
</label>
</div>
<div>
in the spec, when I blur the element I expect to get one error, but "controller.errors" is always an empty array. and I get this for the failed message :
Error: Expected 0 to be 1.
UPDATE 1:
I tried to validate manually, so I added this in my spec :
.then(()=>
viewModel.controller.validate({object: viewModel.user, propertyName: 'firstName' })
)
and it works fine, but the blur and change functions don't trigger validation.
UPDATE 2:
I changed it like "Sayan Pal" suggested. and it works now but with a tiny problem. when I "blur" the element once it shows one error. but when I "blur" several elements ( let's say three ) it doesn't show the last error. in this case controller.errors.length would be 2.
I can blur the last element two times to get the correct length of errors. but I think there should be a better solution.
.then(() => blur(viewModel.firstName))
.then(() => blur(viewModel.userName))
.then(() => blur(viewModel.lastName))
.then(() => blur(viewModel.lastName))
I think instead of using createCustomEvent you simply need to do element.dispatchEvent(new Event("blur"));. Same goes for change event.
This has always worked for me, and hope it will help you too :)
On related note, I use a default ValidationController generator factory method that ensures the default trigger as follows.
import { validateTrigger, ValidationControllerFactory } from "aurelia-validation";
...
const validationController = validationControllerFactory.createForCurrentScope();
validationController.changeTrigger(validateTrigger.changeOrBlur);
Update after OP updated the question
It is difficult to say why it is happening, without debugging. As I don't see any imminent problem in your test code, my assumption is that it is a timing issue. The main idea is that you need to wait for the change to happen. There are several ways you can do it, all of those needs change in how you are asserting.
One way to do it is to employ a promise with a timeout that polls in a regular interval for the change. And then wait for the promise.
Or you can use TaskQueue to queue your assertion, and after the assertion call done. This looks something like below.
new TaskQueue().queueMicroTask(() => {
expect(foo).toBe(bar);
done();
});
Other alternative is to use cypress as an e2e test framework. Out of the box, Cypress waits for the change to happen until times out.
Choose what best fits your need.
I would like to ask if can be tested calling the right function dependent on the condition with sinon or mocha. For example I have class Knight and I want to know if a function (knightRun) is called, when parameter 'data' is true.
export class Knight {
createKnight(data,reducer) {
if (data) {
this.knightRun(reducer);
} else if (!data) {
this.knightFight(reducer);
}
}
private knightFight(reducer) {
// do something
}
private knightRun(reducer) {
// do something
}
}
You can use spies to check whether a particular function has been called. Sinon.js is a library which provides a way to spy on functions when writing unit tests for your JavaScript.
e.g.
describe('Knight class', () => {
it('should call knightRun when data is false', () => {
const knight = new Knight().createKnight(false, null)
sinon.spy(knight, "knightRun")
assert(knight.knightRun.calledOnce)
})
it('should call knightFight when data is true', () => {
const knight = new Knight().createKnight(true, null)
sinon.spy(knight, "knightFight")
assert(knight.knightFight.calledOnce)
})
})
As an aside, the private keyword is not valid JavaScript.