how to unit test subscription to a BehaviourSubject in angular - angular6

I have a UserManagementService which exposes an Observable of a BehaviourSubject.
this.userSignInState$ = this.signInStateSubject.asObservable();
I subscribe to userSignInState in a nav component.
constructor(public userManagementService: UserManagementService, private fb:FormBuilder, private helper:HelperService) {
this.userSignInStateSubscription = this.userManagementService.userSignInState$.subscribe(
(result:Result)=> {
console.log("In nav - result from user signin state ",result);
let subscribed:UserSigninState = result.additionalInfo;
console.log("new user signin state received:", subscribed);
this.userLoggedIn = subscribed.isSignedIn;
if(subscribed.isSignedIn && subscribed['additional-info'] !== ''){
this.profile = JSON.parse(subscribed['additional-info']) as UserProfileAPI
}
if(!subscribed.isSignedIn && subscribed['additional-info'] !== ''){
// let error:ServerResponseAPI = JSON.parse(subscribed['additional-info']) as ServerResponseAPI
//let errorMessage:string = this.helper.userFriendlyErrorMessage(error);
this.navEvent.emit(new NavContext(subscribed['additional-info']));
}
},
(error:ServerResponseAPI)=>{
console.log("got error from the Observable: ",error);
let errorMessage:string = this.helper.userFriendlyErrorMessage(error);
this.navEvent.emit(new NavContext(errorMessage));
// this.userloggedIn =false;
},
()=>{ //observable complete
console.log("observable completed")
//this.userloggedIn =false;
});
}
I want to unit test nav. The spec should test that the component subscribes to userSignInState$ and handles Result correctly. How do I do this? As this is a unit test, I don't want to use the real UserManagementService
I wrote the following spec
fit('should subscribe to user sign in state observable',()=>{
let userManagementService = TestBed.get(UserManagementService);
let navComponent:NavComponentComponent = component;
console.log('component is ',navComponent);
navComponent.userLoggedIn = false;
let dummyUserProfile = new UserProfileAPI(new User('fn','ln','test#test.com'));
userManagementService.signInStateSubject.next(new Result('success',(new UserSigninState(true,JSON.stringify(dummyUserProfile ))).toString));
expect(navComponent.userLoggedIn).toBe(true)
});
but I got error Expected undefined to be true.
I don't understand why userLoggedIn is undefined. I have declared it in the nav class
export class NavComponentComponent implements OnInit {
userLoggedIn:boolean;
...
}
I set it in ngOnInit.
ngOnInit(){
this.userLoggedIn = false;
...
}
I also moved the subscription logic to ngOnInit but that doesn't work either and gives the same result.

The issue was with the way I was creating Result. I should have not used .toString with userSignInState. From another question I posted in SO, "reference to .toString without () is just a reference to that function, so if you log that you get the code for that function." Also, "toString() will not work as userSignInState doesn't have a meanigful string representation and is defaulting to [object Object]". I removed toString and the code worked as additional-info is of type any

Related

How to avoid general Function type in angular

I'm using eslint and it states that using 'Function' as a type is unsafe.
Is there a better way of doing this, so that I don't use the function type:
I have this confirmation dialog that appears when I'm trying to delete something:
export class DialogConfirmationComponent {
message: string = "";
txtBtnConfirmation: string = "Confirm";
actionConfirm: Function = () => { };
actionCancel: Function = () => { };
}
And when I click on the confirm button on the html this actionConfirm Function is called.
I want it to be generic so that I can call this dialog for different components and uses, and define the action using bind on the component, like this:
export class ItemCardComponent {
constructor(
private dialog: MatDialog
) { }
public openDeleteDialog(item: Item): void {
const dialogRef = this.dialog.open(DialogConfirmationComponent, {
width: '500px',
panelClass: 'no-padding-dialog',
autoFocus: false
});
dialogRef.componentInstance.message= "Are you sure?";
dialogRef.componentInstance.txtBtnConfirmation = "Delete";
dialogRef.componentInstance.actionConfirm = this.delete.bind(this, item);
}
private delete(item: Item): void {
// ToDo: delete
}
}
So far it's working, but is there a better way of doing it without using the Function type?
You could use this if the function is very general and you are unsure of the number and type of parameters or the nature of the return type:
(...args: any[]) => any
However, if you are aware of the return type, you may use this in its place. Note that the return type in this case is void:
(...args: any[]) => void
If you want to boost type safety, you may also replace any with types. In this example, you are presuming that the parameters are of the type number or string and that the return type is unknown.
(...args: (string & number)[]) => unknown

Angular how do I use *ngFor with the Async pipe?

Hi I'm having problems with using the asynchronous ngFor, I've got the simplest example of this thing, an array of objects that is obtained from a server onInit, and I want to iterate on int once it arrives,this is how I've written it on the template:
<p *ngFor="let msg of messages | async">test</p>
I mean it looks ok to me but apparently not, here's the ts part:
export class ChatComponent implements OnInit {
url = 'http://localhost:8080';
otherUser?: User;
thisUser: User = JSON.parse(sessionStorage.getItem('user')!);
channelName?: string;
socket?: WebSocket;
stompClient?: Stomp.Client;
newMessage = new FormControl('');
messages?: Observable<Array<Messaggio>>;
constructor(
private route: ActivatedRoute,
private userService: UserService,
private http:HttpClient
) {}
ngOnInit(): void {
this.userService
.getUserByNickname(this.route.snapshot.paramMap.get('user')!)
.subscribe((data) => {
this.otherUser = data;
this.otherUser.propic = "data:image/jpeg;base64,"+ this.otherUser.propic;
this.connectToChat();
});
}
connectToChat() {
const id1 = this.thisUser.id!;
const nick1 = this.thisUser.nickname;
const id2 = this.otherUser?.id!;
const nick2 = this.otherUser?.nickname!;
if (id1 > id2) {
this.channelName = nick1 + '&' + nick2;
} else {
this.channelName = nick2 + '&' + nick1;
}
this.loadChat();
console.log('connecting to chat...');
this.socket = new SockJS(this.url + '/chat');
this.stompClient = Stomp.over(this.socket);
this.stompClient.connect({}, (frame) => {
//func = what to do when connection is established
console.log('connected to: ' + frame);
this.stompClient!.subscribe(
'/topic/messages/' + this.channelName,
(response) => {
//func = what to do when client receives data (messages)
let data:Messaggio = JSON.parse(response.body);
console.log(data);
//this.messages.push(data);
//this.messages = this.messages.slice();
}
);
});
}
loadChat(){
let messages: Array<Messaggio>;
this.http.post<Array<Messaggio>>(this.url+'/getMessages' , this.channelName).subscribe(data =>{
messages = data;
console.log(messages);
})
}
the section regarding the question is the loadChat method which is called in a method called in the onInit, so basically it is called in the on init, and the declaration of the array
point is the array gets defined I even print it on the console but the html page doesn't do jack
Make sure your message object is of type Observable.
and
Add a null check before looping over it with a ngIf
once you messages observable has some data this below code will work fine
<div *ngIf="(messages | async)">
<p *ngFor="let msg of messages | async">test</p>
</div>
Thanks to those who are still answering this but I solved it from the first comment and the problem was: I'm stupid and I assigned the data from the server to an array local to the method instead of the property of the component, if I did that it would have worked from the begininng
lmao

How do I verify a json object loaded into a typescript class is correct?

I want to make sure the JSON I load to my typescript classes is valid.
Also, my classes have some logic, so I want want them to remain classes and not become interfaces.
I also need type checks and required/not required checks.
Right now I just load an object to my constructor and manually check each field.
Is there a way to do it by using the type information from typescript?
I tried looking at the generated javascript files but it's just javascript and the type information is already gone there.
Perhaps there's a way to use it in compile time? at that time, typescript knows the type.
I found a few libraries that do it, but none of them deals with the type of the property as well as required or not.
class MyObject {
constructor(json: any) {
if (typeof json.id == 'number') {
this.id = json.id;
} else {
throw new Error('ID is required');
}
if (json.name != undefined) {
if (typeof json.name == 'string') {
this.name = json.name;
} else {
throw new Error('Name exists, but it is not a string!');
}
}
}
id: number; // required
name?: string; // optional
}
try {
let m1: MyObject = {
id: 123
}; // works but m1 is not a class
let isMyObject = m1 instanceof MyObject ? '' : 'not ';
console.log(`m1 is ${isMyObject}instance of MyObject`);
let m2 = new MyObject({
"id": 123
});
let m3 = new MyObject({
"id": 123,
"name": "Mickey"
});
let m4 = new MyObject({
// this will throw an error
});
console.log('OK!');
} catch (error) {
console.log("Error: " + error.message);
}

LocalStorage: get every value from each key

I want to go through every key and get the name value from each key.
This is how my LocalStorage looks like.
key: 3 Value:
{"name":"Kevin","country":"Canada","about":"Test","image":""}
key: 4 Value:
{"name":"Homer","country":"Canada","about":"Test","image":""}
I want to getboth of these names and add them to my array. I tried it with this method:
for(var key in localStorage){
let user = JSON.parse(localStorage.getItem(key));
this.users.push(user);
}
Error I get is:
SyntaxError: Unexpected token e in JSON at position 1
var keys = Object.keys(localStorage);
keys.forEach(key=>{
var json_str =localStorage.getItem(key)
try {
var abc = JSON.parse(json_str);
this.user = abc;
} catch (e) {
console.log(e)
}
})
when you say I want to getboth of these names, i don't get it but either way you can try something like:
var keys = Object.keys(localStorage);
for(var i=0;i<keys.length;i++){
var key = keys[i];
console.log(key, localStorage[key]);
//store here "both names" where you want them
//you can also access each element with localStorage[key].name, localStorage[key].country, etc.
}
This is a refinement of Robert's answer.
Just enumerate all of the values (The keys themselves do not matter) in localStorage that have a name property that is a string. Then return that array.
Based on your own answer, you likely have inconsistent mutable state as moving your temporary variable to instance scope should not impact your situation.
function getUsers() {
return Object.values(localStorage)
.map(json => {
try {
return JSON.parse(json);
}
catch (e) {
return undefined;
}
})
.filter((user?: any): user is {name: string} => user && typeof user.name === 'string');
}
const users = getUsers();
You can consider using my library ngx-store to deal with localStorage, sessionStorage, cookies and a bit more in Angular. To achieve what you want you will be able to store whole array in storage or just use code like the below with your current data structure:
import { LocalStorageService } from 'ngx-store';
export class Example {
public users: Array<any> = [];
constructor(public localStorageService: LocalStorageService) {
this.localStorageService.utility.forEach((value, key) => this.users.push(value));
}
}
Really, it can be just that simple ;)
You can use method hasOwnProperty('propertyName') to check name available or not in object. Then perform the operation that you want.
let localStorage = {
"key1": {"name":"Kevin","country":"Canada","about":"Test","image":""},
"key2": {"name":"Homer","country":"Canada","about":"Test","image":""}
}
for(let key of Object.keys(localStorage)){
if(localStorage[key].hasOwnProperty('name')){
console.log(localStorage[key]['name']);
}
}
const allItems = []
const keys = Object.keys(window.localStorage); // all keys
keys.forEach(key=> {
const item = JSON.parse(localStorage.getItem(key) + ''); //item with type Object
allItems.push(item);
});
console.log(allItems) // arry of object
I manage to solve it, was a simple error by always initializing a new let user inside the loop.
I moved out the user and the rest of the code works.
user: any;
getUsers():void{
for(var key in localStorage){
this.user = JSON.parse(localStorage.getItem(key));
this.users.push(this.user);
}
}

Angular can't read property data of undefined

I'm having an issue with calling my data from a json file. When I click a button to have it appear in a textarea, it does nothing for the first click, but works like expected after that. What the program does is gets an id from dashboard and based on that id grabs different json file to pull in.
The program shows an error of:
ERROR TypeError: Cannot read property 'data' of undefined
at StepService.webpackJsonp.102.StepService.populateList (step.service.ts:69)
at CalibrationDetailComponent.webpackJsonp.101.CalibrationDetailComponent.next
step.service.ts
private jsonData: any; //Json data
public list: String[] = []; //Holds the list of steps
public listLength: number; //Length of the list
public listCurrent: number = 0; //Current step the list is on
//Gets the json file
public getJson(): Observable<any> {
return this.http.get(this.jsonUrl)
.map(response => response.json());
}
//Subscribe
public subScribe2Json() {
this.getJson().subscribe(data => (this.jsonData = data));
}
//Populates the list from the json so I can pull out specific steps
public populateList() {
this.jsonData.data.forEach(element => { //The line that throws the error
this.list.push(element.name);
});
this.listLength = this.list.length;
}
//Returns the mainStepText with the current step
public getJsonData(): String {
this.mainStepText = this.list[this.listCurrent];
return this.mainStepText;
}
calibration-detail.component.ts
next button method
next() { //Advances step
this.stepService.subScribe2Json();
if (this.stepService.listCurrent < 1) { //Makes sure only runs once to populate the list
this.stepService.populateList(); //Populates list from the json array
}
if (this.stepService.listCurrent < this.stepService.listLength) { //make sure dont go past number of steps
this.stepService.subScribe2Json(); //Sub to list
this.mainStepText = this.stepService.getJsonData(); //Grab the data from the list and output to main textarea
this.stepService.listCurrent ++; //Increments the step
This is not a complete solution but an answer to what the problem is. And direct your thought into the right direction depending on what you want to achieve.
You call
this.stepService.subScribe2Json();
if (this.stepService.listCurrent < 1) {
...
this calls the first method and immediately the second without waiting for the data. And then of course it fails because it is not there yet.
Depending on your use case you could either return the Observable (maybe change it to a Promise,... not 100% sure) and then:
return this.getJson().subscribe(data => (this.jsonData = data));
and something like
this.stepService.subScribe2Json().then(/* do all stuff here */);
or initialize
private jsonData: any = [];
but here of course you don't have anything on the first run.