How to write marble test for this custom RxJS operator used in redux-observalbe Epic - rxjs

I need to write marble test for my custom operator used in this loadEpic epic - this helps me to avoid problems that action INITIALiZE sometimes is dispatched to late and i getting LOAD_FAILURE:
loadEpic: Epic<ExamManagementAction, ExamManagementAction, RootState> = (
action$,
state$
) =>
action$.pipe(
filter(isActionOf(load)),
waitFor(state$),
switchMap(() =>
this.load(state$).pipe(
map(loadSuccess),
catchError(error => of(loadFailure({ error })))
)
)
);
and this is how i wrote my waitFor operator which works fine:
const waitFor = <T>(
state$: Observable<RootState>
): OperatorFunction<T, T> => source$ =>
source$.pipe(
switchMap(value =>
state$.pipe(
filter(state => state.navigation.initialized),
take(1),
mapTo(value)
)
)
);
can you help me to write this test with rxjs-marbles/jest or any similar approach? many thanks!

You describe three streams of events:
states (mock them with simple objects)
actions (again, you may use any JS value as a mock)
filtered actions (the same object as in 2)
Then you expect your operator to transform 2 to 3 with toBeObservable matcher. That's it.
it('should reject given values until navigation is initialized', () => {
const state$ = hot(' -i--u--u-i-- ', {u: {navigation: {initialized: false}}, i: {navigation: {initialized: true}}});
const action$ = hot(' v----v--v--- ', {v: load});
const expect$ = cold(' -v-------v-- ', {v: load});
expect(action$.pipe(waitFor(state$))).toBeObservable(expect$);
});
Note how I've formatted my code so one stream is described under another. It really helps with long sequences of events.
You might also write separate specs for edge cases. It depends on what behavior you want to test.

Related

How do I obtain the value of another observable, without it affecting the parent stream's return value

I want to make an http request, without affecting the value of the parent stream:
const mockHttpCall = () => of('http-result');
of('result1')
.pipe(
switchMap(() =>
mockHttpCall().pipe(
tap((innerResult) => console.log('innerResult: ', innerResult))
)
)
)
ie: this stream should return result1 rather than http-result.
How can I do this? I think something might be possible using withLatestFrom and map back to the original value but not sure if it is good idea.
Stackblitz: https://stackblitz.com/edit/rxjs-lrcuse?devtoolsheight=60&file=index.ts
You can just map back to the original value:
of('result1')
.pipe(
switchMap((result) =>
mockHttpCall().pipe(
tap((innerResult) => console.log('innerResult: ', innerResult)),
map(() => result)
)
)
)
.subscribe(console.log)

Should we avoid nested rxjs operators? One case which I cannot test

I have written the following effect in my Angular app which uses rxjs. On MyActions.myAction, I receive an object containing a property ids - an array of ids - and for each id I want to send an HTTP request via this.myApiService.getResource, which returns an Observable<Resource>. I want then to collect all results in an array, and dispatch another action passing the array.
public loadResources$: Observable<MyAction> = this.actions$.pipe(
ofType(MyActions.myAction),
switchMap(({ ids }) => from(ids).pipe(
mergeMap(id => this.myApiService.getResource(id)),
toArray()
)),
map(resources) => MyActions.resourcesLoaded({ resources } )),
);
The code above does the job, but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that.
The reason I wonder that is that I am having problems writing a test for it. I wrote the test below but I cannot make it pass.
it('should dispatch an resourcesLoaded action with the resources', () => {
const ids = ['5f7c723832758b859bd8f866'];
const resources = [{} as Resource];
const values = {
l: MyActions.loadResources({ ids }),
t: ids[0],
o: MyActions.resourcesLoaded({ resources })
};
actions = hot('--l------------', values);
const get$ = cold(' -------t-----', values);
const expected = cold('---------o-----', values);
myApiService.getResource.withArgs(ids[0]).returns(get$);
expect(myEffects.loadResources$).toBeObservable(expected);
});
The error I get is:
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
Error: Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
at <Jasmine>
at compare (http://localhost:9876/Users/jacopolanzoni/Documents/Development/myProject/node_modules/jasmine-marbles/index.js:91:1)
at <Jasmine>
but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that
I'd say it depends on what you want to achieve, at least in this case.
of([1,2,3]).pipe(mergeAll(), switchMap(value => http.get(...)))
differs from
of([1,2,3]).pipe(switchMap(ids => from(ids).pipe(mergeMap(...))))
In the first scenario, each inner observable will be discarded by the next value(except for the last value), so only 3 will resolve.
In the second scenario, it will process all of them, because you explode the array in the inner observable(which is managed by swtichMap, so the only way its inner observable will be discarded is if a new outer value(e.g another array of ids) is emitted by the source).
A case where nesting is not necessary is:
of([1,2,3])
.pipe(
// whenever you want to explode an array,
// it does not matter which higher order operator you use
// since the operation is **synchronous**
// so, `mergeAll`, `concatAll`, `switchAll` should work the same
mergeAll(),
mergeAll(id => this.apiService.useId(id))
)
// same as
of([1,2,3])
.pipe(
mergeMap(ids => from(ids).pipe(mergeMap(id => this.apiService.useId(id))))
)
As you can see, in this case, switchMap has been replaced with mergeMap.
I have found out my test was failing because toArray was waiting for the observable returned by getResource (i.e., httpClient.get) to complete. Replacing t with (t|) fixes the test:
it('should dispatch an resourcesLoaded action with the resources', () => {
const ids = ['5f7c723832758b859bd8f866'];
const resources = [{} as Resource];
const values = {
l: MyActions.loadResources({ ids }),
t: ids[0],
o: MyActions.resourcesLoaded({ resources })
};
actions = hot('--l------------', values);
const get$ = cold(' -------(t|)-----', values);
const expected = cold('---------o-----', values);
myApiService.getResource.withArgs(ids[0]).returns(get$);
expect(myEffects.loadResources$).toBeObservable(expected);
});
Yet, the first part of my question, i.e. whether it's good practice or not to nest operators like that, still stands.

Cancel repeated subscription in mergeMap

How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).

Conditionally producing multiple values based on item value and merging it into the original stream

I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';

RxJs Observable: Execute function if empty/filtered

I've got an Observable that listens to some user input from a text box. If the observed string's length is >=3 (filter), it executes some HTTP call (switchMap).
Now I'd like to detect somehow if the user input has been filtered. Reason:
If the HTTP call has been done, it should show the results.
If the user input got filtered (== is invalid), it should clear the results.
Here's the code I'd like to have (see: ifFiltered):
this.userInput.valueChanges
.filter(val => val && val.length >= 3)
.ifFiltered(() => this.results = [])
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
I know, I could place that logic within the filter function for this simple example. But what if I have 10 different filters?
Did I miss any method that satisfies my needs?
Thanks in advance!
Either use partition like here RxJS modeling if else control structures with Observables operators
Or instead of filter use map and pipe the object if the former filter condition is true or null otherwise. so you can catch the null where ever you want in your chain with a filter.
Last option call some function in the else part of the filter function
We've had a similar case and tried it with partition as mentioned above but found it much handier to use throw here. So for your code
this.userInput.valueChanges
.do(val => {
if (!val || val.length < 3) {
throw new ValueTooShortError();
}
})
.switchMap(val => getDataViaHTTP())
.do(val => this.results = val)
.catch(() => this.results = [])
.subscribe();
I suggest having a common event stream, creating two filtered streams, and merging the two before subscription:
var o = this.userInput.valueChanges;
var empty= o.filter(t=> t.length < 3)
.map(t=>[])
var nonempty = o.filter(t=> t.length >= 3)
.switchMap(t=> getDataViaHTTP());
empty.merge(nonempty).subscribe(val => this.results = val);
I found another nice solution for my use case using Validators:
(I know that this is no solution using Observables as the question stated. Instead it's using Angular2 features to workaround the problem nicely.)
this.userInput.validator = Validators.compose([
Validators.required,
Validators.minLength(3)
]);
this.userInput.valueChanges
.filter(val => this.userInput.valid)
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
Now I can use the userInput.valid property and/or the userInput.statusChanges Observable to keep track of the input value.
May be it's late, but wanted to post for the members still seeking a more cleaner approach to validate IF EMPTY inside .map:
of(fooBar).pipe(
map(
(val) =>
({
...val,
Foo: (val.Bar
? val.Foo.map((e) => ({
title: e.Title,
link: e.Link,
}))
: []) as fooModal[],
}));
This code returns a empty array if val.bar is missing, but it's just an example you can use any validation & expression instead.

Resources