Testing Observable Callback to Change Object State - javascript

I have a service that does the following:
updateProperties(properties: any) {
return this.http.put(environment.adminApiURLPrefix+'api/v1/properties', properties);
}
In my component I have two objects, providers and providerProperties. Providers contains a key value pair of API providers and their status, whilst providerProperties is the actual response that is returned back from the API (has to be stored as any subsequent requests need the entire object).
import { Component, OnInit } from '#angular/core';
import { ProviderService } from '../../services/provider.service';
import { Status } from 'src/app/models/status.enum';
import { AlertService } from 'src/app/services/alert.service';
#Component({
selector: 'app-providers',
templateUrl: './providers.component.html',
styleUrls: ['./providers.component.scss']
})
export class ProvidersComponent implements OnInit {
providers: any;
providerProperties: any;
status: Status;
constructor(private providerService: ProviderService, private alertService: AlertService) {
this.providers = {
'API1': false,
'API2': false,
'API3': false
}
this.status = Status.Loading;
}
ngOnInit() {
this.providerService.getProperties().subscribe((response: any) => {
this.setProviderValues(response);
this.status = Status.Ready;
},
error => {
this.alertService.error('Error');
this.status = Status.Error;
})
}
setProviderValues(response: any) {
this.providerProperties = response;
Object.keys(this.providers).forEach(key => {
let providerStatus: string = this.providerProperties[key.toLowerCase() + '.enabled'];
if(providerStatus == 'true' || providerStatus == 'false') {
this.providers[key] = providerStatus == 'true' ? true : false;
} else {
this.alertService.error('Error.');
this.status = Status.Error;
return;
}
});
}
changeProviderProperty(provider: string, isEnabled: boolean) {
let providerKey: string = this.providerDisplayNameToPropertyKey(provider);
let tempProviderProperties = Object.assign({}, this.providerProperties);
tempProviderProperties[providerKey] = isEnabled ? true : false;
this.providerService.updateProperties(tempProviderProperties).subscribe(
response => {
this.setProviderValues(response);
this.alertService.success('Successfully '+ (isEnabled ? 'Enabled ' : 'Disabled ') + provider);
},
err => {
if(err['status'] == 0) {
this.alertService.error('Error');
} else {
let errorArray = err['error']['Errors']['Error'];
for(let errorKey in errorArray) {
let errorValue = errorArray[errorKey];
this.alertService.error('Error Updating Properties: ' + errorValue['ReasonCode'] + ' ' + errorValue['Description']);
}
}
}
)
}
providerDisplayNameToPropertyKey(provider: string) {
return provider.toLowerCase() + '.enabled';
}
}
I have the following test:
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '#angular/core/testing';
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { ProvidersComponent } from './providers.component';
import { ProviderService } from 'src/app/services/provider.service';
import { of, Observable, throwError } from 'rxjs';
import { AlertComponent } from '../shared/alert/alert.component';
import { RouterTestingModule } from '#angular/router/testing';
import { AlertService } from 'src/app/services/alert.service';
import { HttpErrorResponse } from '#angular/common/http';
describe('ProvidersComponent', () => {
let component: ProvidersComponent;
let providerService: ProviderService;
let alertService: AlertService;
let fixture: ComponentFixture<ProvidersComponent>;
let getPropertiesSpy: jasmine.Spy<() => Observable<String>>;
let changeProviderPropertySpy: jasmine.Spy<(provider: string, isEnabled: boolean) => Promise<void>>;
let providerProperties:any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ProvidersComponent,
AlertComponent
],
imports: [
HttpClientTestingModule,
RouterTestingModule
],
providers: [
ProviderService,
AlertService
]
})
.compileComponents();
}));
beforeEach(() => {
providerService = TestBed.get(ProviderService);
alertService = TestBed.get(AlertService);
fixture = TestBed.createComponent(ProvidersComponent);
providerProperties = {
'api1.enabled': 'true',
'api2.enabled': 'true',
'api3.enabled': 'false',
};
component = fixture.componentInstance;
component.providers = {
'API1': false,
'API2': false,
'API3': false
};
getPropertiesSpy = spyOn(providerService, 'getProperties').and.callFake(() => {
return of(providerProperties);
});
changeProviderPropertySpy = spyOn(component, 'changeProviderProperty');
fixture.detectChanges();
});
it('should enable provider', fakeAsync(() => {
spyOn(providerService, 'updateProperties').and.returnValue(of({
'api1.enabled': 'true',
'api2.enabled': 'false',
'api3.enabled': 'false'
}));
component.changeProviderProperty('API1', true);
fixture.detectChanges();
expect(component.providers['API1']).toEqual(true);
}));
});
For some reason the state of the objects won't change. I'm fairly sure the observable isn't being subscribed to or I am not waiting for the response.
Update
After spying on the provider service and checking to see if it had been called after I call changeProviderProperties, it turns out that it was never called.
Update 2
Spied on the providerDisplayNameToPropertyKey method and found that it's not being called for whatever reason.

The method component.changeProviderProperty contains asynchronous code. Because you're aware of that, you're running your test in the fakeAsync zone. What you're missing is a call to tick that simulates the asynchronous passage of time for the timers in the fakeAsync zone.
it('should enable provider', fakeAsync(() => {
...
component.changeProviderProperty('API1', true);
tick();
...
}));

Related

LocalStorage not displaying array of todolist after refreshing page

I'm currently developing a ToDo List using Angular 8. When I enter in a task for my ToDo list it saves it in the Localstorage but when I refresh my page my tasks are gone but are still stored inside the localstorage of the browser.
How do I keep my tasks from disappearing after refreshing the browser, when they're still saved in LocalStorage?
import { Component, OnInit } from '#angular/core';
import { ToDo, IToDo } from './todo.model';
import { HttpClient } from '#angular/common/http';
import { LocalStorageService } from '../localStorageService';
import { ActivatedRoute, Router } from '#angular/router';
import { IUser } from '../login/login.component';
import { ToastService } from '../toast/toast.service';
#Component({
// tslint:disable-next-line: component-selector
selector: 'todolist',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class ToDoComponent implements OnInit {
todos: Array<IToDo> = [];
inputtask = "";
toDoParams = '';
localStorageService: LocalStorageService<IToDo>;
currentUser: IUser;
modal: any;
constructor(
private http: HttpClient,
private activatedRoute: ActivatedRoute,
private router: Router) {
this.localStorageService = new LocalStorageService('todos');
}
private toastService: ToastService;
async ngOnInit() {
const currentUser = this.localStorageService.getItemsFromLocalStorage('user');
console.log('from todos component', currentUser);
if (currentUser == null) {
this.router.navigate(['login']);
}
}
// Creating a to do item by clicking on the Enter Button
addToDo(todo: string) {
const td = {
id: 1,
task: todo,
editing: false
}
if (todo === '') {
alert('You must enter in a task TO DO!')
} else {
this.todos.push(td);
}
this.saveItemsToLocalStorage(this.todos);
}
delete(index: number) {
this.todos.splice(index, 1);
console.log("index", index);
this.saveItemsToLocalStorage(this.todos);
}
clear() {
this.todos = [];
console.log('index', this.todos)
this.saveItemsToLocalStorage(this.todos);
}
getItemsFromLocalStorage(key: string) {
const savedToDo = JSON.parse(localStorage.getItem(key));
console.log('from getItemsFromLocalStorage savedItems', savedToDo);
return this.localStorageService.getItemsFromLocalStorage(key);
return savedToDo;
}
saveItemsToLocalStorage(todos: Array<IToDo>) {
todos = this.sortByID(todos);
return this.localStorageService.saveItemsToLocalStorage(todos);
const savedToDo = localStorage.setItem('todos', JSON.stringify(todos));
console.log('from saveItemsToLocalStorage savedToDos: ', savedToDo);
return savedToDo;
}
sortByID(todos: Array<IToDo>) {
todos.sort((prevToDo: IToDo, presToDo: IToDo) => {
return prevToDo.id > presToDo.id ? 1 : -1;
});
console.log('the sorted ToDos', this.todos);
return this.todos;
}
logout() {
// clear localStorage
this.localStorageService.clearItemFromLocalStorage();
// navigate to login page
this.router.navigate(['']);
}
}
Here is the LocalStorageService file below
export class LocalStorageService<T> {
constructor(private key: string) {
}
saveItemsToLocalStorage(todos: Array<T> | T) {
const savedToDos = localStorage.setItem(this.key, JSON.stringify(todos));
console.log('from saveItemsToLocalStorage savedToDos: ', savedToDos);
return savedToDos;
}
getItemsFromLocalStorage(key?: string) {
let savedItems;
if (key != null) {
const items = null;
savedItems = JSON.parse(localStorage.getItem(key));
console.log('from getItemFromLocalStorage key: ', key, 'savedItems: ', savedItems);
} else {
savedItems = JSON.parse(localStorage.getItem(this.key));
}
return savedItems;
}
clearItemFromLocalStorage(key?: string) {
if (key != null) {
const items = null;
localStorage.setItem(key, JSON.stringify(items));
} else {
localStorage.clear();
}
}
}
Hey so when the page reloads angular loses context of the view so it has to render it again. You have all the working code here to make it work you just have to change the ngOnInit page load event so it reads them again and binds them to the property so angular can show them in the UI. Also bare in mind you call clearItemFromLocalStorage on logout so this won't grab them if they logout and then log back in, but i guess that was expected due to the code you have wrote.
The below should slot in and work for you:
async ngOnInit() {
const currentUser = this.localStorageService.getItemsFromLocalStorage('user');
console.log('from todos component', currentUser);
if (currentUser == null) {
await this.router.navigate(['login']);
} else {
// if user is logged in go and find any items from local storage and bind
// to the view
const toDoItems = this.localStorageService.getItemsFromLocalStorage('todos');
if (toDoItems && Array.isArray(toDoItems)) {
this.todos = toDoItems;
}
}
}

How to execute a function every time an ion-tab button gets clicked

I am trying to build a barcode scanner app with the Ionic framework. The function in scan.page.ts works fine, but unfortunately just once. After I scanned one barcode, the scanner wont open anymore.
I think it might be an issue with the ion-tabs. They only trigger the scan() function once. Afterwards the function will not get executed anymore.
scan.page.ts
import { Component, OnInit } from '#angular/core';
import { BarcodeScanner } from '#ionic-native/barcode-scanner/ngx';
import { DataServiceService } from '../../app/data-service.service';
import { Toast } from '#ionic-native/toast/ngx';
import { Router } from '#angular/router';
import { Platform, AlertController } from '#ionic/angular';
import * as moment from 'moment';
#Component({
selector: 'app-scan',
templateUrl: './scan.page.html',
styleUrls: ['./scan.page.scss'],
})
export class ScanPage implements OnInit {
productViews: any = {};
productViewsbyUser: any[] = [];
isProdcutsAvailable = true;
selectedProduct: any;
isCameraOpen = false;
showScan = false;
products: any[] = [];
productFound = true;
displayUserName: any;
exitModalDisplayed = false;
constructor(
private barcodeScanner: BarcodeScanner,
private router: Router,
public platform: Platform,
private toast: Toast,
public dataService: DataServiceService,
public alertCtrl: AlertController) {
console.log(`Scan Page called`);
}
ngOnInit() {
this.dataService.getProducts()
.subscribe((response) => {
this.products = <any[]><unknown>response;
console.table(this.products);
});
this.scan();
this.handleBackButton();
}
getMoment() {
return moment().milliseconds(0);
}
// Start scanning procedure
scan() {
this.selectedProduct = {};
this.isCameraOpen = true;
this.showScan = true;
this.barcodeScanner.scan().then((barcodeData) => {
setTimeout(() => {
this.isCameraOpen = false;
}, 500);
if (barcodeData.cancelled) {
return;
}
console.log(`barcodeData`, barcodeData);
this.selectedProduct = this.products.find(product => product.plu === barcodeData.text);
if (this.selectedProduct !== undefined) {
this.selectedProduct.scannedAt = this.getMoment().toISOString();
// this.selectedProduct.userName = this.displayUserName(); // TO TEST !!!
this.productFound = true;
// insert product views with firebase generated based key
this.dataService.insertProductViewAnalaytics(this.selectedProduct)
.subscribe(() => {
console.log(`Product view analytics inserted in Firebase`);
this.initScanHistoryData();
});
} else {
this.productFound = false;
this.toast.show(`Product not found`, '5000', 'center').subscribe(
toast => {
console.log(toast);
}
);
}
}, (err) => {
setTimeout(() => {
this.isCameraOpen = false;
}, 1000);
this.toast.show(err, '5000', 'center').subscribe(
toast => {
console.log(toast);
}
);
});
}
async initScanHistoryData() {
this.dataService.getProductViewsForUser()
.subscribe((response) => {
this.productViews = response;
const userProductViews = [];
// tslint:disable-next-line: forin
for (const key in this.productViews) {
userProductViews.push(this.productViews[key]);
}
userProductViews.sort(function (a, b) {
return moment(b.scannedAt).diff(moment(a.scannedAt));
// ENTER USER NAME HERE???
});
this.productViewsbyUser = userProductViews;
console.log('user productViews ', userProductViews);
if (this.productViewsbyUser.length) {
this.isProdcutsAvailable = true;
} else {
this.isProdcutsAvailable = false;
}
console.log('productViews ', this.productViews);
});
}
handleBackButton() {
this.platform.backButton.subscribeWithPriority(9999, () => {
console.log('exit');
if (this.exitModalDisplayed || this.isCameraOpen) {
return;
}
this.exitModalDisplayed = true;
const alert = this.alertCtrl.create({
header: 'Do you want to Exit?',
// message: 'Do you want Exit?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary',
handler: () => {
console.log('Cancel clicked');
this.exitModalDisplayed = false;
}
},
{
text: 'Yes',
handler: () => {
console.log('Confirm Okay');
navigator['app'].exitApp();
}
}
]
});
alert.then(x => x.present());
});
}
}
tabs.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { FormsModule } from '#angular/forms';
import { Routes, RouterModule } from '#angular/router';
import { IonicModule } from '#ionic/angular';
import { TabsPage } from './tabs.page';
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{ path: 'profile', loadChildren: '../profile/profile.module#ProfilePageModule' },
{ path: 'products', loadChildren: '../products/products.module#ProductsPageModule' },
{ path: 'scan', loadChildren: '../scan/scan.module#ScanPageModule' },
]
},
{
path: '',
redirectTo: '/tabs/products',
pathMatch: 'full'
}
];
#NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes)
],
declarations: [TabsPage]
})
export class TabsPageModule {}
tabs.page.html
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="products">
<ion-icon name="md-list"></ion-icon>
<ion-label>Products</ion-label>
<!-- <ion-badge>6</ion-badge> -->
</ion-tab-button>
<ion-tab-button tab="scan">
<ion-icon name="md-qr-scanner"></ion-icon>
<ion-label>Scan</ion-label>
</ion-tab-button>
<ion-tab-button tab="profile">
<ion-icon name="md-person"></ion-icon>
<ion-label>Profile</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
I expect that the scanner gets opened every time the scan tab button gets pressed.
Right now, your scan() function is only executed when ngOnInit() is called.
If you want to execute it every time a user clicks on the tab you can do something like:
<ion-tab-button tab="scan" (click)="scan()">
<ion-icon name="md-qr-scanner"></ion-icon>
<ion-label>Scan</ion-label>
</ion-tab-button>
try to another methode, that is using ionViewWillEnter() in your target tab, that is solve my problem too

Filter not filtering data in mat-table

I have created a mat-table to display list of Jobs.
Now I want to add a mat-filter to search a job using date or JobId.
However the code that I have written doesn't seem to work.
It does not throw any errors and it doesn't filter data.
HTML Code:
<mat-form-field>
<input
matInput
(keyup)="applyFilter($event.target.value)"
placeholder="Search"
/>
</mat-form-field>
<mat-table [dataSource]="jobExecutionList">
...
Typescript Code:
jobExecutionList: any = [];
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
Whole Typescript file :
import { Component, OnInit } from "#angular/core";
import { MatTableDataSource } from "#angular/material";
import { GlobalAppSateService } from "../../services/globalAppSate.service";
import { DataService } from "../../services/data.service";
import { SnakBarComponent } from "../custom-components/snak-bar/snak-
bar.component";
import { DataSource } from "#angular/cdk/collections";
import { Observable, of } from "rxjs";
import {
animate,
state,
style,
transition,
trigger
} from "#angular/animations";
import { RecommendationService } from "../recommendation-service.service";
import { MessageService } from '../../services/message.service';
#Component({
selector: "app-job-execution-screen",
templateUrl: "./job-execution-screen.component.html",
styleUrls: ["./job-execution-screen.component.scss"],
animations: [
trigger("detailExpand", [
state(
"collapsed",
style({ height: "0px", minHeight: "0", visibility: "hidden" })
),
state("expanded", style({ height: "*", visibility: "visible" })),
transition(
"expanded <=> collapsed",
animate("225ms cubic-bezier(0.4, 0.0, 0.2, 1)")
)
])
]
})
export class JobExecutionScreenComponent implements OnInit {
displaySpinner: boolean = false;
jobId: string;
jobExecutionList: any = [];
jobExecStatDisplayedColumns = [
"jobId",
"executionDate",
"previousTimePeriod",
"afterTimePeriod",
"status",
"actions",
"spinner"
];
public selectedElem: any;
projectjobId: any = 1;
jobExecutionStat: any;
executionDate: string = new Date().toISOString().slice(0, 10);
executeJobStop: any;
changeStatus: any;
newStatus: any;
isExpansionDetailRow = (i: number, row: Object) =>
row.hasOwnProperty("detailRow");
expandedElement: any;
constructor(
private dataService: DataService,
public globalAppSateService: GlobalAppSateService,
private snakbar: SnakBarComponent,
private recommendationService: RecommendationService,
private messageService: MessageService
) {}
ngOnInit() {
const project = JSON.parse(this.dataService.getObject("project"));
if (project != null) {
this.globalAppSateService.onMessage(project);
}
// API to get list of Running Jobs
this.recommendationService
.getJobExecutionStatList(this.projectjobId)
.subscribe(data => {
this.jobExecutionList = data;
console.log(this.jobExecutionList);
// this.jobExecutionStat = new ExampleDataSource();
});
}
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
stop_exec_job(element) {
if (element.status == "Running" || element.status == "Pending") {
//Api to stop Job Execution
this.recommendationService
.stopJobExecution(element.jobId, "Cancelled")
.subscribe(data => {
this.executeJobStop = data;
//this.changeStatus.push(this.executeJobStop);
// this.newStatus = new ExampleDataSource();
});
this.displaySpinner = false;
element.status = "Cancelled";
this.snakbar.statusBar("Job Execution Stopped", "Sucess");
} else {
this.snakbar.statusBar("Job Failed to start", "Failure");
}
}
// Will need it for mat-progress bar
// stop_exec_job2() {
// this.stop_exec_job(this.selectedElem);
// this.displaySpinner = false;
// }
re_run_job(element) {
if (
element.status == "Cancelled" ||
element.status == "Completed" ||
element.status == "Executed" ||
element.status == "FINISHED"
) {
//Api to Re-Run Job Execution
this.recommendationService
.stopJobExecution(element.jobId, "Running")
.subscribe(data => {
this.executeJobStop = data;
//this.changeStatus.push(this.executeJobStop);
// this.newStatus = new ExampleDataSource();
});
this.displaySpinner = true;
element.status = "Running";
this.snakbar.statusBar("Job Execution Started", "Sucess");
this.messageService.messageReceived$.subscribe(data => {
this.snakbar.statusBar(
'Platform job status - ' + data,
'Info'
);
//this.isLoadingResults = false;
});
} else {
this.snakbar.statusBar("Job Failed to start", "Failure");
}
}
}
export interface Element {
jobId: number;
executionDate: string;
previousTimePeriod: string;
afterTimePeriod: string;
status: string;
}
This is the Whole typescript file.
Based on different comment, you need to do:
dataSource: MatTableDataSource<any>;
And then when you get the data:
this.dataSource = new MatTableDataSource(/** YOUR DATA **/);
In your example:
import { Component, OnInit } from "#angular/core";
import { MatTableDataSource } from "#angular/material";
import { GlobalAppSateService } from "../../services/globalAppSate.service";
import { DataService } from "../../services/data.service";
import { SnakBarComponent } from "../custom-components/snak-bar/snak-
bar.component";
import { DataSource } from "#angular/cdk/collections";
import { Observable, of } from "rxjs";
import {
animate,
state,
style,
transition,
trigger
} from "#angular/animations";
import { RecommendationService } from "../recommendation-service.service";
import { MessageService } from '../../services/message.service';
#Component({
selector: "app-job-execution-screen",
templateUrl: "./job-execution-screen.component.html",
styleUrls: ["./job-execution-screen.component.scss"],
animations: [
trigger("detailExpand", [
state(
"collapsed",
style({ height: "0px", minHeight: "0", visibility: "hidden" })
),
state("expanded", style({ height: "*", visibility: "visible" })),
transition(
"expanded <=> collapsed",
animate("225ms cubic-bezier(0.4, 0.0, 0.2, 1)")
)
])
]
})
export class JobExecutionScreenComponent implements OnInit {
displaySpinner: boolean = false;
jobId: string;
jobExecutionList: MatTableDataSource<any>;
jobExecStatDisplayedColumns = [
"jobId",
"executionDate",
"previousTimePeriod",
"afterTimePeriod",
"status",
"actions",
"spinner"
];
public selectedElem: any;
projectjobId: any = 1;
jobExecutionStat: any;
executionDate: string = new Date().toISOString().slice(0, 10);
executeJobStop: any;
changeStatus: any;
newStatus: any;
isExpansionDetailRow = (i: number, row: Object) =>
row.hasOwnProperty("detailRow");
expandedElement: any;
constructor(
private dataService: DataService,
public globalAppSateService: GlobalAppSateService,
private snakbar: SnakBarComponent,
private recommendationService: RecommendationService,
private messageService: MessageService
) {}
ngOnInit() {
const project = JSON.parse(this.dataService.getObject("project"));
if (project != null) {
this.globalAppSateService.onMessage(project);
}
// API to get list of Running Jobs
this.recommendationService
.getJobExecutionStatList(this.projectjobId)
.subscribe(data => {
this.jobExecutionList = new MatTableDataSource(data);
console.log(this.jobExecutionList);
// this.jobExecutionStat = new ExampleDataSource();
});
}
applyFilter(filterValue: string) {
this.jobExecutionList.filter = filterValue.trim().toLowerCase();
}
I already have an example for this kind, you can look over this.
Mat-Table-stackblitz

Shared service example Angular 5

I know this question has been asked several times, but problem is that nobody tried to make a some fiddle or show results of code. This is what i have, i need to update values in other component based on value in some other component, but that is not just value,I have call function again in some other component.
I have some component that goes to database and update values, on second hand I have other component that read those values from database from service.
This is example of my code
tasks.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { environment } from '../../environments/environment';
import { Tasks } from './tasks';
#Injectable()
export class TasksProvider {
constructor(private http: HttpClient) { }
createNewTask(name: Name) : Observable<any> {
return this.http.post(environment.apiUri + 'tasks', { name, finished: false },
{ responseType: 'text' });
}
updateTask(id: Id, name: Name, finished: boolean) : Observable<any> {
return this.http.put(environment.apiUri + 'tasks/' + id, { name, finished },
{ responseType: 'text' });
}
getAllTasks(): Observable<Tasks[]> {
return this.http.get(environment.apiUri + 'tasks')
.map<any, Tasks[]>(data => data.map(Tasks.fromObject));
}
}
app.component.html
<app-tasks-list></app-tasks-list>
<app-tasks-add-new></app-tasks-add-new>
As you may see I have not child components, that is my main problem
tasks-list.component.ts
import {Component} from '#angular/core';
import { Tasks } from '../services/tasks';
import { TasksProvider } from '../services/tasks.service';
#Component({
selector: 'app-tasks-list',
templateUrl: './tasks-list.component.html',
styleUrls: ['./tasks-list.component.scss']
})
export class TasksListComponent {
tasks: Array<Tasks>;
constructor(private tasksProvider: TasksProvider) { }
ngOnInit() {
this.getTasksList();
}
displayedColumns: string[] = ['id', 'name', 'finished'];
private getTasksList() {
this.tasksProvider.getAllTasks()
.subscribe(tasks => {
this.tasks = tasks;
});
}
public updateCheckboxValue(id: number, name: string, event: any){
this.tasksProvider.updateTask(id, name, event.checked).subscribe(
result => {},
() => {
alert('Something went wrong');
})
}
}
tasks-add-new.component.ts
import { Component, OnInit, Inject } from '#angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '#angular/material';
import { Tasks } from '../services/tasks';
import { TasksProvider } from '../services/tasks.service';
export interface DialogData {
name: string;
}
#Component({
selector: 'app-tasks-add-new',
templateUrl: './tasks-add-new.component.html',
styleUrls: ['./tasks-add-new.component.scss']
})
export class TasksAddNewComponent implements OnInit {
ngOnInit() {
}
constructor(public dialog: MatDialog, private tasksProvider: TasksProvider) {}
openDialog(): void {
const dialogRef = this.dialog.open(TasksAddNewDialog, {
width: '250px',
data: {name: this.animal}
});
dialogRef.afterClosed().subscribe(result => {
this.name = result
this.tasksProvider.createNewTask(this.name).subscribe(
result => {},
() => {
alert('Something went wrong');
})
}
}
}
#Component({
selector: 'tasks-add-new-dialog',
templateUrl: 'tasks-add-new-dialog.html'
})
export class TasksAddNewDialog {
constructor(
public dialogRef: MatDialogRef<TasksAddNewDialog>,
#Inject(MAT_DIALOG_DATA) public data: DialogData) {}
onNoClick(): void {
this.dialogRef.close();
}
}
You see now when i call function in tasks-add-new.component.ts like
this.tasksProvider.createNewTask(this.name).subscribe(
result => {},
() => {
alert('Something went wrong');
})
I need to call again function in tasks-list.component.ts
private getTasksList() {
this.tasksProvider.getAllTasks()
.subscribe(tasks => {
this.tasks = tasks;
});
}
Does any body have idea how i can do that the best practice?
On of the possible approach is to use Subjects.
1) Store task list on the service and provide subscribable Subject
private tasks: Array<Task>;
public $tasks: BehaviorSubject<Array<Task>>;
constructor(private http: HttpClient) {
this.$tasks = new BehaviorSubject([]);
...
}
getAllTasks() {
this.http.get(environment.apiUri + 'tasks')
.subscribe(data => {
this.tasks = data;
this.$tasks.next(this.tasks);
});
}
updateTask(params) {
this.http.post(/* params */).subscribe((task) => {
this.tasks = this.tasks.map(t => t.id !== task.id ? t : task);
this.$tasks.next(this.tasks);
});
}
createTask(...) {
// again, do a request, update this.tasks and call $tasks.next
...
}
2) Make one service Subject subscription on the component instead of multiple service methods Observable listeners and update component's list automatically each time the service source has been changed
tasks: Array<Tasks>;
constructor(private tasksProvider: TasksProvider) {
this.tasksProvider.$tasks.subscribe(tasks => this.tasks = tasks);
}
ngOnInit() {
this.tasksProvider.getAllTasks();
}
public updateCheckboxValue(id: number, name: string, event: any){
this.tasksProvider.updateTask(id, name, event.checked);
}

Angular component's test fails with timeout

I am trying to test my component. The components receives an observable string and object as an input and iterates over this object and makes some calculations.
The component works fine in a browser, but throws an error when I execute the unit test:
Chrome 65.0.3325 (Mac OS X 10.13.1) ERROR
Disconnected, because no message in 10000 ms.
Chrome 65.0.3325 (Mac OS X 10.13.1): Executed 1 of 116 DISCONNECTED (10.136 secs / 0.092 secs)
Chrome 65.0.3325 (Mac OS X 10.13.1) ERROR
Chrome 65.0.3325 (Mac OS X 10.13.1): Executed 1 of 116 DISCONNECTED (10.136 secs / 0.092 secs)
So i’ve started simplifying the component but still don't understand why the tests are failing.
My component's template:
<div *ngIf="hasDestinationFilter"></div>
<div *ngFor="let shipment of (filteredShipments$ | async)"></div>
TypeScript:
import { Component, Input, OnInit } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/operator/map';
import { ShipmentFilterService } from '../../services/shipment-filter/shipment-filter.service';
#Component({
selector: 'search-results',
templateUrl: './search-results.component.html',
})
export class SearchResultsComponent implements OnInit {
#Input() searchResults$: Observable<any>;
#Input() destination$: Observable<string>;
filteredShipments$: Observable<any[]>;
hasDestinationFilter = false;
constructor(private shipmentFilterService: ShipmentFilterService) {}
ngOnInit() {
this.filteredShipments$ = Observable
.combineLatest(this.searchResults$, this.destination$)
.map(([ { shipments }, destination ]) => {
this.hasDestinationFilter = this.shipmentFilterService.hasDestinationFilter(shipments);
if (this.hasDestinationFilter) {
return shipments;
}
return shipments;
});
}
}
My unit test:
import { NO_ERRORS_SCHEMA } from '#angular/core';
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { ShipmentFilterService } from '../../services/shipment-filter/shipment-filter.service';
import { SearchResultsComponent } from './search-results.component';
describe('SearchResultsComponent', () => {
let mockedHasDestinationFilter;
const mockedShipment = {
collectedDate: null,
collectionCity: null,
collectionDate: null,
consignmentKey: null,
consignmentNumber: null,
customerReference: null,
deliveryDueDate: null,
deliveryTown: null,
destinationCountry: null,
fedexNumber: null,
fromDate: null,
originCountry: null,
pieceQuantity: null,
podFound: null,
revisedDeliveryDate: null,
shipmentId: null,
signatory: null,
signatoryFound: false,
statusData: [],
toDate: null,
trackId: null,
};
const shipmentFilterServiceStub = {
hasDestinationFilter: () => mockedHasDestinationFilter,
};
beforeEach(async(() => {
TestBed
.configureTestingModule({
declarations: [
SearchResultsComponent,
],
providers: [
{ provide: ShipmentFilterService, useValue: shipmentFilterServiceStub },
],
schemas: [
NO_ERRORS_SCHEMA,
],
})
.compileComponents();
}));
function getComponent(destination = null) {
let component: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
fixture = TestBed.createComponent(SearchResultsComponent);
component = fixture.componentInstance;
component.searchResults$ = Observable.of({
query: {
input: 'abc123',
},
shipments: [
mockedShipment,
mockedShipment,
mockedShipment,
mockedShipment,
mockedShipment,
],
});
component.destination$ = Observable.of(destination);
fixture.detectChanges();
return fixture;
}
describe('optionally filters shipments by destination', () => {
it('shows all shipments when there is no destination filter', () => {
mockedHasDestinationFilter = false;
const component = getComponent();
expect(component.debugElement.queryAll(By.css('pb-shipment')).length).toBe(5);
});
fit('shows all shipments when there is destination filter, but no destination has been chosen', () => {
mockedHasDestinationFilter = true;
const component = getComponent('');
});
});
});
It's hard to understand where the problem since unit tests fails with timeout, but I noticed if I remove <div *ngIf="hasDestinationFilter"></div> or <div *ngFor="let shipment of (filteredShipments$ | async)"></div> from my templates the tests doesn't fail with timeout. Where can be my mistake?
Try to initialize the values of your inputs with default values
#Input() searchResults$: Observable<any> = Observable.from("");
#Input() destination$: Observable<string> = Observable.from("");

Categories