I have started learning how to test angular projects. So far basic unit testing is working fine for me but for the dependency testing especially when API services are injected into the component I am facing issue for providing HttpClient. I have tried different solutions but none is working for me.
Service
// Present in HttpClientService file
getDisposition() {
return this.http.get<{ message: string, data: { dispositionList: Disposition[] } }>(`${this.URL}/disposition/get`);
}
// Present in FileProcessService file
deleteMedia(media: string) {
return this.http.delete<{ message: string }>(`${this.URL}/certificate/delete?certificate=${media}`);
}
add-edit-activity.component.ts
import { HttpEventType } from '#angular/common/http';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '#angular/core';
import { FormBuilder, Validators } from '#angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { DomSanitizer, SafeUrl } from '#angular/platform-browser';
import { AssignedPerson } from '#model/assigned-person.model';
import { Disposition } from '#model/disposition.model';
import { mimeTypes } from '#model/mime-type';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { DeleteComponent } from '#shared/delete/delete.component';
import { CustomErrorStateMatcher } from '#validator/error-state-matcher';
import { ToastrService } from 'ngx-toastr';
#Component({
selector: 'app-add-edit-activity',
templateUrl: './add-edit-activity.component.html',
styleUrls: ['./add-edit-activity.component.css']
})
export class AddEditActivityComponent implements OnInit {
constructor(private fb: FormBuilder, private sanitizer: DomSanitizer, private dialogRef: MatDialogRef<AddEditActivityComponent>, #Inject(MAT_DIALOG_DATA) public data: any,
private _http: HttpClientService, private _fileService: FileProcessService, private toastr: ToastrService, private dialog: MatDialog,) { }
re = new RegExp(/^[a-zA-Z-]*/, 'g');
ISOstamp = { T: ' ', Z: '000' };
matcher = new CustomErrorStateMatcher();
dispositionList: Disposition[] = [];
assignedPersonList: AssignedPerson[] = [];
filterAssignedPersonList: AssignedPerson[] = [];
uploaded = false;
uploadProgress = false;
fileURL: SafeUrl;
activityForm = this.fb.group({
gaugeId: [this.data.gaugeId, Validators.maxLength(9)], createTimeStamp: [{ value: new Date().toISOString().replace(/[TZ]/g, m => this.ISOstamp[m]), disabled: true }],
user: [{ value: sessionStorage.getItem('username'), disabled: true }], disposition: ['', [Validators.required, Validators.maxLength(30)]],
assignedPersonName: ['', Validators.maxLength(30)], department: ['', Validators.maxLength(20)],
shift: ['', Validators.maxLength(1)], remark: ['', Validators.maxLength(50)],
calibrationDate: ['', Validators.maxLength(10)], attachment: ['', Validators.maxLength(255)]
});
#ViewChild('file') certificate: ElementRef;
ngOnInit(): void {
this.data.type.match(this.re)[0] === 'Update' && this.setFormValues();
this._http.getDisposition().subscribe(response => this.dispositionList = response.data.dispositionList);
this._http.getAssignedPerson().subscribe(response => this.assignedPersonList = this.filterAssignedPersonList = response.data.assignedToList);
}
get GaugeId() {
return this.activityForm.get('gaugeId');
}
get TimeStamp() {
return this.activityForm.get('createTimeStamp');
}
get Attachment() {
return this.activityForm.get('attachment');
}
get Disposition() {
return this.activityForm.get('disposition');
}
get DispositionValue() {
return this.dispositionList.map(e => e.dispositionType).indexOf(this.Disposition.value) < 0;
}
get AssignedTo() {
return this.activityForm.get('assignedPersonName');
}
get AssignedToValue() {
return this.assignedPersonList.map(e => `${e.firstName} ${e.lastName}`).indexOf(this.Disposition.value) < 0;
}
private async setFormValues() {
this.activityForm.patchValue({ ...this.data });
if (this.data.attachment) {
this.uploadProgress = true;
this.uploaded = true;
await this.fetchUploadedFile(this.data.attachment, mimeTypes[this.data.attachment.split('.')[1]]);
this.activityForm.markAsPristine();
}
}
searchAssignedPerson(event) {
if (event.target.value) {
this.filterAssignedPersonList = [];
for (let person of this.assignedPersonList) {
if (person.firstName.toLowerCase().startsWith(event.target.value.toLowerCase())) {
this.filterAssignedPersonList.push(person);
}
}
} else { this.filterAssignedPersonList = this.assignedPersonList }
}
upload(event) {
const file: File = event.target.files[0];
this.certificate.nativeElement.value = '';
if (file.size > (20 * 1000 * 1000)) { // Checking if File size is above 20MB
this.toastr.error('Size of ' + file.name + ' is above 20MB');
return;
}
const fd = new FormData();
fd.append('certificate', file, file.name);
this.processAttachment(fd);
}
private processAttachment(file: FormData) {
this._fileService.uploadMedia(file).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) { this.uploadProgress = true }
if (event.type === HttpEventType.Response) {
let media = event.body.data.Certificate;
this.fetchUploadedFile(media.fileName, media.fileType);
this.toastr.info(event.body.message);
}
}, error => {
this.toastr.error(error.error.message);
this.uploadProgress = false;
});
}
private async fetchUploadedFile(file: string, mimeType: string) {
try {
let response = await this._fileService.getMedia(file).toPromise();
this.fileURL = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(new Blob([response], { type: mimeType })));
this.uploaded = true;
this.uploadProgress = false;
this.activityForm.patchValue({ attachment: file });
this.Attachment.markAsDirty();
} catch (error) {
this.uploadProgress = false;
this._fileService.processError(error.error)
}
}
deleteFile() {
this.dialog.open(DeleteComponent, {
width: '350px', disableClose: true
}).afterClosed().subscribe(response => {
if (response) {
this._fileService.deleteMedia(this.Attachment.value).subscribe(response => {
this.toastr.info(response.message);
this.activityForm.patchValue({ attachment: '' });
this.Attachment.markAsDirty();
this.uploaded = false;
}, error => this.toastr.error(error.error.message));
}
});
}
async doAction() {
let message = '';
if (this.data.type.match(this.re)[0] === 'Add') {
message = await (await this._http.addActivityLog(this.activityForm.getRawValue()).toPromise()).message;
} else {
message = await (await this._http.updateActivityLog(this.GaugeId.value, this.TimeStamp.value, this.activityForm.getRawValue()).toPromise()).message;
}
this.dialogRef.close(message);
}
}
add-edit-activity.component.spec.ts
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let _http: HttpClientService, _file: FileProcessService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },,
{ provider: HttpClientService, useValue: null },
{ provider: FileProcessService, useValue: null }
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
_http = fixture.debugElement.injector.get(HttpClientService);
_file = fixture.debugElement.injector.get(FileProcessService);
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
let spy = spyOn(_http, 'getDisposition').and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
let spy = spyOn(_file, 'deleteMedia').and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
The error that am getting for those 2 tests (I'm providing the error of 1 test case, the same is coming for the 2nd one also):
SHOULD mock http service
AddEditActivityComponent
Error: Invalid provider for the NgModule 'DynamicTestModule' - only instances of Provider and Type are allowed, got: [..., ..., ...,
..., ?undefined?, ..., ...]
at throwInvalidProviderError (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:240:1)
at providerToFactory (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11550:1)
at providerToRecord (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11521:1)
at R3Injector.processProvider (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11424:1)
Error: : could not find an object to spy upon for getDisposition() Usage: spyOn(, ) Error: :
could not find an object to spy upon for getDisposition() Usage:
spyOn(, )
at
Anyone, please help what's the exact error happening with my test cases? Stuck here for almost 2 days.
You have to avoid using a 'null' value when providing services in the TestBed, that what is causing your problem.
The best way to mock Angular services is to create SpyObj instances and then use them like what is done Angular tests documentation.
Here is how would be your spec file if you were using SpyObj:
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let httpSpy: jasmine.SpyObj<HttpClientService>;
let fileSpy: jasmine.SpyObj<FileProcessService>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },
{
provider: HttpClientService,
useValue: jasmine.createSpyObj('HttpClientService', ['getDisposition', 'getAssignedPerson', 'addActivityLog', 'updateActivityLog'])
},
{
provider: FileProcessService,
useValue: jasmine.createSpyObj('FileProcessService', ['uploadMedia', 'getMedia', 'deleteMedia', 'processError'])
}
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
httpSpy = fixture.debugElement.injector.get(HttpClientService) as jasmine.SpyObj<HttpClientService>;
fileSpy = fixture.debugElement.injector.get(FileProcessService) as jasmine.SpyObj<FileProcessService>;
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
httpSpy.getDisposition.and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
fileSpy.deleteMedia.and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
Related
I'm quite new to ml5 and p5 libraries and during implementation to my Angular project I'm receiving this error:
TypeError: this.objectDetector.detect is not a function
After logging objectDetector object console shows this:
ZoneAwarePromise {__zone_symbol__state: null, __zone_symbol__value: Array(0)}
p5 drawing working good but combined with ml5 is not working.
Here's my component code:
import { Component, OnInit } from '#angular/core';
import * as p5 from 'p5';
declare let ml5: any;
#Component({
selector: 'app-new-found',
templateUrl: './new-found.component.html',
styleUrls: ['./new-found.component.scss']
})
export class NewFoundComponent implements OnInit {
objectDetector;
img;
constructor(
) { }
ngOnInit(): void {
const sketch = (s) => {
s.preload = () => {
this.objectDetector = ml5.objectDetector('cocossd');
console.log('detector object is loaded', this.objectDetector);
this.img = s.loadImage('https://i.imgur.com/Mzh4cHR.jpg');
}
s.setup = () => {
s.createCanvas(700, 700).parent('test-canvas');
this.objectDetector.detect(this.img, this.gotResult);
s.image(this.img, 0, 0);
};
s.draw = () => {
};
}
let canvas = new p5(sketch);
}
gotResult(error, results) {
if (error) {
console.error(error);
} else {
console.log(results);
//drawResults(results);
}
}
}
ml5 library is imported in <HEAD> of my index.html file.
Does someone know how to get rid of this error?
Thank you.
Finally I figured it out. The ml5.objectDetector('cocossd'); function must be marked as await because it takes quite long time to execute. Below is working code:
import { Component, OnInit } from '#angular/core';
import * as p5 from 'p5';
declare let ml5: any;
#Component({
selector: 'app-new-found',
templateUrl: './new-found.component.html',
styleUrls: ['./new-found.component.scss']
})
export class NewFoundComponent implements OnInit {
objectDetector;
img;
constructor(
) { }
async ngOnInit(): Promise<void> {
this.objectDetector = await ml5.objectDetector('cocossd');
const sketch = (s) => {
s.preload = () => {
console.log(ml5);
console.log('detector object is loaded', this.objectDetector);
this.img = s.loadImage('https://i.imgur.com/Mzh4cHR.jpg');
}
s.setup = () => {
s.createCanvas(700, 700).parent('test-canvas');
this.objectDetector.detect(this.img, this.gotResult);
s.image(this.img, 0, 0);
};
s.draw = () => {
};
}
let canvas = new p5(sketch);
}
gotResult(error, results) {
if (error) {
console.error(error);
} else {
console.log(results);
//drawResults(results);
}
}
}
It is possible that the library has not fully loaded yet. I would create a polling technique here where you keep checking if the value has been initialized and only then proceed.
This is the code I use for polling that xola script has loaded:
let subscription = interval(1000)
.pipe(timeout(3 * 60 * 1000))
.subscribe({
next: () => {
if (this.window.xola) {
const xola = this.window.xola();
subscription.unsubscribe();
this.xolaSubject.next(xola);
}
},
error: (error) => {
if (error instanceof TimeoutError) {
console.error('Xola took too long to load, check your connection.');
}
subscription.unsubscribe();
},
});
i have a messaging room application that create a discussion chat foreach room between users signed in with same room ,
i m facing this error :
Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
i used 3 functions :
getChatMessages() : to get all chat messages from firestore for users
with same room
getCurrentRoom():to get the room of the connected user
getUsers(): return all users with same room
chat.services.ts
import { Injectable } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/compat/auth';
import { AngularFirestore } from '#angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { Timestamp } from 'rxjs/internal/operators/timestamp';
import { switchMap,map, timestamp, filter } from 'rxjs/operators';
import { query, orderBy, limit, where } from "firebase/firestore";
import firebase from 'firebase/compat/app';
export interface User {
uid: string;
email: string;
displayName:string;
username?:string;
room?:string
}
export interface Message {
createdAt: firebase.firestore.FieldValue;
id: string;
from: string;
msg: string;
fromName: string;
myMsg: boolean;
}
#Injectable({
providedIn: 'root'
})
export class ChatService {
currentUser: User ;
currentRoom:string="";
updatedRoom:string="";
constructor(private afAuth: AngularFireAuth, private afs: AngularFirestore) {
this.afAuth.onAuthStateChanged((user) => {
this.currentUser=user;
console.log("current email"+this.currentUser.email);
});
}
async signup({ username,email, password,room }): Promise<any> {
const credential = await this.afAuth.createUserWithEmailAndPassword(
email,
password
);
const uid = credential.user.uid;
return this.afs.doc(
`users/${uid}`
).set({
uid,
email: credential.user.email,
username:username,
room:room,
})
}
signIn({ email, password }) {
return this.afAuth.signInWithEmailAndPassword(email, password);
}
signOut(): Promise<void> {
return this.afAuth.signOut();
}
addChatMessage(msg) {
return this.afs.collection('messages').add({
createdAt:firebase.firestore.FieldValue.serverTimestamp(),//firebase.default.firestore.Timestamp,
msg: msg,
from: this.currentUser.uid
});
}
async getChatMessages() {
let users = [];
return (await this.getUsers()).pipe(
switchMap(res => {
users = res;
console.log("resssssss"+res);
return this.afs.collection('messages', ref => ref.orderBy('createdAt','asc')).valueChanges({ idField: 'id' }) as Observable<Message[]>;
}),
map(messages => {
console.log("messages"+messages);
// Get the real name for each user
for (let m of messages) {
m.fromName = this.getUserForMsg(m.from, users);
m.myMsg = this.currentUser.uid === m.from;
}
return messages
})
)
}
public async getCurrentRoom() {
await this.afs.collection('users', ref => ref.where("email", "==", this.currentUser.email)).get().toPromise()
.then(snapshot => {
snapshot.forEach(doc => {
this.currentRoom=JSON.parse(JSON.stringify(doc.data())).room;
console.log("current room" + this.currentRoom);
});
});
}
public async getUsers() {
console.log("this room" + this.currentRoom);
return this.afs.collection('users', ref => ref.where("room", "==", this.currentRoom)).valueChanges({
idField: 'uid'
}) as Observable < User[] > ;
}
private getUserForMsg(msgFromId, users: User[]): string {
for (let usr of users) {
if (usr.uid == msgFromId) {
return usr.username ?? 'undefind';
}
}
return 'Deleted';
}
}
my chat.page.ts :
import { Component, OnInit, ViewChild } from '#angular/core';
import { IonContent } from '#ionic/angular';
import { Observable } from 'rxjs';
import { ChatService } from '../chat.service';
import { Router } from '#angular/router';
import { AngularFireStorage, AngularFireUploadTask } from '#angular/fire/compat/storage';
import { AngularFirestore, AngularFirestoreCollection } from '#angular/fire/compat/firestore';
import { finalize, tap } from 'rxjs/operators';
export interface FILE {
name: string;
filepath: string;
size: number;
}
#Component({
selector: 'app-chat',
templateUrl: './chat.page.html',
styleUrls: ['./chat.page.scss'],
})
export class ChatPage implements OnInit {
ngFireUploadTask: AngularFireUploadTask;
progressNum: Observable<number>;
progressSnapshot: Observable<any>;
fileUploadedPath: Observable<string>;
room:any;
files: Observable<FILE[]>;
ImgtoSend:any;
FileName: string;
FileSize: number;
isImgUploading: boolean;
isImgUploaded: boolean;
private ngFirestoreCollection: AngularFirestoreCollection<FILE>;
#ViewChild(IonContent) content: IonContent;
messages:any=[];
newMsg = '';
constructor(private angularFirestore: AngularFirestore,
private angularFireStorage: AngularFireStorage,private chatService: ChatService, private router: Router) {
this.isImgUploading = false;
this.isImgUploaded = false;
this.ngFirestoreCollection = angularFirestore.collection<FILE>('filesCollection');
this.files = this.ngFirestoreCollection.valueChanges();
}
ngOnInit() {
this.messages= this.chatService.getChatMessages();
}
sendMessage() {
this.chatService.addChatMessage(this.newMsg).then(() => {
this.newMsg = '';
this.content.scrollToBottom();
});
}
signOut() {
this.chatService.signOut().then(() => {
this.router.navigateByUrl('/login', { replaceUrl: true });
});
}
fileUpload(event: FileList) {
const file = event.item(0)
if (file.type.split('/')[0] !== 'image') {
console.log('File type is not supported!')
return;
}
this.isImgUploading = true;
this.isImgUploaded = false;
this.FileName = file.name;
const fileStoragePath = `filesStorage/${new Date().getTime()}_${file.name}`;
console.log("filestoragepath"+fileStoragePath);
const imageRef = this.angularFireStorage.ref(fileStoragePath);
console.log("image ref"+imageRef);
this.ngFireUploadTask = this.angularFireStorage.upload(fileStoragePath, file);
this.ImgtoSend=this.FileName;
console.log("image to Send"+this.ImgtoSend);
this.progressNum = this.ngFireUploadTask.percentageChanges();
this.progressSnapshot = this.ngFireUploadTask.snapshotChanges().pipe(
finalize(() => {
this.fileUploadedPath = imageRef.getDownloadURL();
console.log("uploaded path"+this.fileUploadedPath);
this.fileUploadedPath.subscribe(resp=>{
this.fileStorage({
name: file.name,
filepath: resp,
size: this.FileSize
});
this.isImgUploading = false;
this.isImgUploaded = true;
},error => {
console.log(error);
})
}),
tap(snap => {
this.FileSize = snap.totalBytes;
})
)
}
fileStorage(image: FILE) {
const ImgId = this.angularFirestore.createId();
this.ngFirestoreCollection.doc(ImgId).set(image).then(data => {
console.log("data"+data);
}).catch(error => {
console.log(error);
});
}
}
enter code here
First, you have to eleminate all the code that is not relevant to the question to help people help you.
Second, the issue is simple:
What the compiler is telling you here is, Hey Mohammed Amir, *ngFor="" is used to loop through an Array of objects while you are passing to it an object literal.
Check the value of the property you bind to *ngFor="let msg of [YourAssumedArrayFromEndpointResponse]" in your template you will find that YourAssumedArrayFromEndpointResponse is not an array. That's why the compiler is complaining
I have a mat table in my angular project just like below:
The data is fetched from server using rest API. The server side is using nodejs and sequelize. When I click the delete button, I want my mat table to refresh with new data. But unfortunately, the refresh data is loaded before the delete is completed, so it looks as if the delete button is not functioning.
Here is my code:
list.component.ts
import {AfterViewInit, ChangeDetectorRef, Component, ViewChild} from '#angular/core';
import {MatPaginator, PageEvent} from '#angular/material/paginator';
import {MatSort} from '#angular/material/sort';
import {MatTableDataSource} from '#angular/material/table';
import { Title } from '#angular/platform-browser';
import { Router } from '#angular/router';
import { ClientService } from 'src/app/core/services/client.service';
import { MainService } from 'src/app/core/services/main.service';
import { Client } from 'src/app/models/client.model';
#Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent implements AfterViewInit {
clients: Client[];
displayedColumns: string[] = ['client_name', 'client_address', 'client_home_phone', 'client_mobile_phone', 'client_status', 'buttons'];
dataSource: MatTableDataSource<Client>;
length = 0;
#ViewChild(MatPaginator) paginator: MatPaginator;
#ViewChild(MatSort) sort: MatSort;
constructor(private changeDetectorRefs: ChangeDetectorRef, private router: Router, private titleService: Title, private mainService: MainService, private clientService: ClientService) {
this.titleService.setTitle('Clients');
this.mainService.setPageTitle('Clients');
this.mainService.setButtonToolbar([
{
'url': 'create',
'icon': 'add',
'color': 'primary'
},
]);
this.clientService.list(25, 0).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
});
}
ngAfterViewInit() {
}
onChangedPage(pageData: PageEvent) {
this.clientService.list(pageData.pageSize, pageData.pageIndex * pageData.pageSize).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.clientService.list(this.paginator.pageSize, 0, filterValue).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
});
}
onUpdate(id: String) {
this.router.navigate(['/client/update/'+id]);
}
onDelete(id: string) {
this.clientService.delete(id).subscribe(response => {
if (response == 200) {
this.clientService.list(this.paginator.pageSize, 0).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
this.changeDetectorRefs.detectChanges();
});
}
});
}
}
client.service.ts
import { HttpClient, HttpHeaders } from "#angular/common/http";
import { Injectable } from "#angular/core";
import { map } from "rxjs/operators";
import { Client } from "src/app/models/client.model";
import { MainService } from 'src/app/core/services/main.service';
#Injectable()
export class ClientService {
constructor(private http: HttpClient, private mainService: MainService) {}
list(limit: number, offset: number, filter?: string) {
let queryParams = '?limit='+limit+'&offset='+offset;
if (filter) {
queryParams += '&filter='+filter;
}
return this.http
.get(this.mainService.getbaseApiUrl()+"client/list" + queryParams)
.pipe(map(responseData => {
const clientArray = [...responseData['rows']];
return {data: clientArray, count: responseData['count']};
}));
}
get(id: string) {
return this.http
.get(this.mainService.getbaseApiUrl()+"client/one/"+id);
}
add(client: Client) {
return this.http
.post(this.mainService.getbaseApiUrl()+'client/add', client, {
headers: new HttpHeaders({
// 'Origin': '*',
//'Access-Control-Allow-Origin': '*',
//'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json',
})
});
}
update(client: Client) {
return this.http
.post(this.mainService.getbaseApiUrl()+'client/edit', client, {
headers: new HttpHeaders({
// 'Access-Control-Allow-Origin': '*',
// 'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json',
})
});
}
delete(id: string) {
return this.http.delete(this.mainService.getbaseApiUrl()+'client/delete/'+id);
}
}
I'm new to Angular and this may look simple, but I couldn't think of any solution. Is there any way for Angular to wait until the deletion is completed then refresh? The temporary solution is to use setTimeout so the mat table will refresh after waiting for X seconds, but I don't think that's a clean solution.
Please help.
UPDATE:
My temporary solution is to reload the current router.
app-routing.module.ts
#NgModule({
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})],
exports: [RouterModule]
})
list.component.ts
ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
}
}
onDelete(id: string) {
this.clientService.delete(id).subscribe(response => {
if (response == 200) {
this.router.navigate(['/client']);
}
});
}
But it will be great if there are other better solutions than this one.
onDelete(id: string) {
this.clientService.delete(id)
.pipe(switchMap(() => this.clientService.list(this.paginator.pageSize, 0)
.subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
this.changeDetectorRefs.detectChanges();
});
I am testing the following service:
import { HttpException, HttpStatus, Injectable, Logger } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { extend } from 'lodash';
import { Repository } from 'typeorm';
import { DriverDTO } from './driver.dto';
import { DriverEntity } from './driver.entity';
#Injectable()
export class DriverService {
private logger = new Logger('DriverService');
constructor(
#InjectRepository(DriverEntity)
private driverRepository: Repository<DriverEntity>,
) { }
async create(clientId: string, data: DriverDTO): Promise<Partial<DriverEntity>> {
let driver = await this.driverRepository.findOne({ where: { clientId, driverId: data.driverId } });
this.logger.log(driver);
if (driver) {
throw new HttpException('Driver already exists', HttpStatus.CONFLICT);
}
driver = this.driverRepository.create(extend({ clientId }, data));
await this.driverRepository.save(driver);
return { driverId: driver.driverId, createdAt: driver.createdAt };
}
}
The service.spec.ts is as follows:
import { Test, TestingModule } from '#nestjs/testing';
import { DriverService } from './driver.service';
import { MockType } from '../mock/mock.type';
import { plainToClass } from 'class-transformer';
import { repositoryMockFactory } from '../mock/repositoryMock.factory';
import { getRepositoryToken } from '#nestjs/typeorm';
import { DriverEntity } from './driver.entity';
import { Repository } from 'typeorm';
import { Pagination } from '../pagination/pagination';
import { HttpException, HttpStatus } from '#nestjs/common';
import { DriverDTO } from './driver.dto';
describe('DriverService', () => {
let service: DriverService;
let driverRepositoryMock: MockType<Repository<DriverEntity>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DriverService,
{ provide: getRepositoryToken(DriverEntity), useFactory: repositoryMockFactory },
],
}).compile();
service = module.get<DriverService>(DriverService);
driverRepositoryMock = module.get(getRepositoryToken(DriverEntity));
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(driverRepositoryMock).toBeDefined();
});
describe('DriverService.create()', () => {
const driverDto: DriverDTO = {
driverId: 'one',
};
const driver: DriverEntity = plainToClass(DriverEntity, {
id: 1,
clientId: 'one',
driverId: 'one',
createdAt: '2019-02-11T04:11:00.766Z',
updatedAt: '2019-02-11T04:11:00.766Z',
});
it('should create driver if it does not already exist', () => {
const response: Partial<DriverEntity> = plainToClass(DriverEntity, {
driverId: driver.driverId,
createdAt: driver.createdAt,
});
driverRepositoryMock.findOne.mockReturnValue(undefined);
driverRepositoryMock.create.mockReturnValue(driver);
expect(service.create('one', driverDto)).resolves.toEqual(response);
expect(driverRepositoryMock.findOne).toHaveBeenCalledTimes(1);
expect(driverRepositoryMock.findOne).toHaveBeenCalledWith({ where: { clientId: 'one', driverId: driverDto.driverId } });
expect(driverRepositoryMock.create).toHaveBeenCalledTimes(1);
expect(driverRepositoryMock.create).toHaveBeenCalledWith({
clientId: driver.clientId,
driverId: driver.driverId,
});
expect(driverRepositoryMock.save).toHaveBeenCalledWith(driver);
});
});
});
But I keep getting the following error:
● DriverService › DriverService.create() › should create driver if it does not already exist
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
237 | expect(driverRepositoryMock.findOne).toHaveBeenCalledTimes(1);
238 | expect(driverRepositoryMock.findOne).toHaveBeenCalledWith({ where: { clientId: 'one', driverId: driverDto.driverId } });
> 239 | expect(driverRepositoryMock.create).toHaveBeenCalledTimes(1);
| ^
240 | expect(driverRepositoryMock.create).toHaveBeenCalledWith({
241 | clientId: driver.clientId,
242 | driverId: driver.driverId,
at Object.it (driver/driver.service.spec.ts:239:43)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 8 passed, 9 total
Snapshots: 0 total
Time: 1.762s, estimated 6s
Ran all test suites matching /src\/driver\/driver.service.spec.ts/i.
I mocked the repository like this:
import { Repository, Entity } from 'typeorm';
import { MockType } from './mock.type';
// #ts-ignore
export const repositoryMockFactory: () => MockType<Repository<any>> = jest.fn(() => ({
findOne: jest.fn(entity => entity),
findAndCount: jest.fn(entity => entity),
create: jest.fn(entity => entity),
save: jest.fn(entity => entity),
}));
I can't seem to find the cause. Any help will be appreciated.
Make your test function async to wait until your service method is completely executed before you check the expectations for your mocks:
it('should create driver if it does not already exist', async () => {
// ^^^^^
// ...
await expect(service.create('one', driverDto)).resolves.toEqual(response);
//^^^^^
I've got a service, which determines the location, it's written as Observable
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
const GEOLOCATION_ERRORS = {
'errors.location.unsupportedBrowser': 'Browser does not support location services',
'errors.location.permissionDenied': 'You have rejected access to your location',
'errors.location.positionUnavailable': 'Unable to determine your location',
'errors.location.timeout': 'Service timeout has been reached'
};
#Injectable()
export class GeolocationService {
public getLocation(opts): Observable<any> {
return Observable.create(observer => {
if (window.navigator && window.navigator.geolocation) {
window.navigator.geolocation.getCurrentPosition(
(position) => {
observer.next(position);
observer.complete();
},
(error) => {
switch (error.code) {
case 1:
observer.error(GEOLOCATION_ERRORS['errors.location.permissionDenied']);
break;
case 2:
observer.error(GEOLOCATION_ERRORS['errors.location.positionUnavailable']);
break;
case 3:
observer.error(GEOLOCATION_ERRORS['errors.location.timeout']);
break;
}
}, opts);
} else {
observer.error(GEOLOCATION_ERRORS['errors.location.unsupportedBrowser']);
}
});
}
}
export var GeolocationServiceInjectables: Array<any> = [
{ provide: GeolocationService, useClass: GeolocationService }
];
Then in my HttpService I want to construct the query URL with the output from location service
import { Observable } from 'rxjs/Observable';
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { GeolocationService } from './location.service';
import { WeatherItem } from '../weather-item/weather-item.model';
export const OpenWeatherMap_API_KEY: string = 'SOME_API_KEY';
export const OpenWeatherMap_API_URL: string = 'http://api.openweathermap.org/data/2.5/forecast';
#Injectable()
export class HttpService {
constructor(private http: Http,
private geolocation: GeolocationService,
#Inject(OpenWeatherMap_API_KEY) private apiKey: string,
#Inject(OpenWeatherMap_API_URL) private apiUrl: string) {
}
prepaireQuery(): void {
this.geolocation.getLocation({ enableHighAccuracy: false, maximumAge: 3 }).subscribe(
(position) => {
let params: string = [
`lat=${position.latitude}`,
`lon=${position.longitude}`,
`APPID=${this.apiKey}`,
].join('&');
// return `${this.apiUrl}?${params}`;
}
);
}
getWeather(): Observable<WeatherItem[]> {
return this.http.get(/*there should be the url*/)
.map((response: Response) => {
return (<any>response.json()).items.map(item => {
const city = {
city: item.city.name,
country: item.city.country,
}
return item.list.map(entity => {
return new WeatherItem({
temp: entity.main.temp,
temMin: entity.main.temp_min,
temMax: entity.main.temp_max,
weatherCond: entity.weather.main,
description: entity.weather.description,
windSpeed: entity.wind.speed,
icon: entity.weather.icon,
city,
})
})
})
})
}
}
export var HttpServiceInjectables: Array<any> = [
{ provide: HttpService, useClass: HttpService },
{ provide: OpenWeatherMap_API_KEY, useValue: OpenWeatherMap_API_KEY },
{ provide: OpenWeatherMap_API_URL, useValue: OpenWeatherMap_API_KEY }
];
The question is how to get the URL before doing request. I've seen solutions with unsubscribe(), but I think thay are not so good. I've thought about merge() but I'm not sure that it's what I really want.
You are probably looking for the mergeMap operator of RxJs.
What mergeMap does is automatically subscribes to the source observable, then lets you work with its result in your inner observable, and then finally flattens your output.
In this example, you call the firstUrl and use the result you get from that request in your second call to secondUrl:
this.http.get(`{firstUrl}`)
.mergeMap(res => this.http.get(`{secondUrl}/{res.json()}`))
.subscribe(...)
I have not made it specific for your code, as I'm not sure of exactly what you want to do. But I hope this will help you on the way!
This can be done using map/flatMap combination:
getWeather(): Observable<WeatherItem[]> {
return this.geolocation.getLocation({ enableHighAccuracy: false, maximumAge: 3 })
.map((position) => {
let params: string = [
`lat=${position.latitude}`,
`lon=${position.longitude}`,
`APPID=${this.apiKey}`,
].join('&');
return `${this.apiUrl}?${params}`;
})
.flatMap(url => this.http.get(url)
.map((response: Response) => {
return (<any>response.json()).items.map(item => {
const city = {
city: item.city.name,
country: item.city.country,
}
return item.list.map(entity => {
return new WeatherItem({
temp: entity.main.temp,
temMin: entity.main.temp_min,
temMax: entity.main.temp_max,
weatherCond: entity.weather.main,
description: entity.weather.description,
windSpeed: entity.wind.speed,
icon: entity.weather.icon,
city,
})
})
})
})
}