how to test RxJs debounced epics? - rxjs

I'm trying to write a test function to a redux-observable epic. The epic works fine inside the app and I can check that it correctly debounces the actions and waits 300ms before emitting. But for some reason while I'm trying to test it with jest the debounce operator triggers immediately. So my test case to ensure that the debounce is working fails.
This is the test case
it('shall not invoke the movies service neither dispatch stuff if the we invoke before 300ms', done => {
const $action = ActionsObservable.of(moviesActions.loadMovies('rambo'));
loadMoviesEpic($action).subscribe(actual => {
throw new Error('Should not have been invoked');
});
setTimeout(() => {
expect(spy).toHaveBeenCalledTimes(0);
done();
}, 200);
});
this is my spy definition.
jest.mock('services/moviesService');
const spy = jest.spyOn(moviesService, 'searchMovies');
beforeEach(() => {
moviesService.searchMovies.mockImplementation(keyword => {
return Promise.resolve(moviesResult);
});
spy.mockClear();
});
and this is the epic
import { Observable, interval } from 'rxjs';
import { combineEpics } from 'redux-observable';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounce';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/interval';
import 'rxjs/add/observable/concat';
import actionTypes from 'actions/actionTypes';
import * as moviesService from 'services/moviesService';
import * as moviesActions from 'actions/moviesActions';
const DEBOUNCE_INTERVAL_IN_MS = 300;
const MIN_MOVIES_SEARCH_LENGTH = 3;
export function loadMoviesEpic($action) {
return $action
.ofType(actionTypes.MOVIES.LOAD_MOVIES)
.debounce(() => Observable.interval(DEBOUNCE_INTERVAL_IN_MS))
.filter(({ payload }) => payload.length >= MIN_MOVIES_SEARCH_LENGTH)
.switchMap(({ payload }) => {
const loadingAction = Observable.of(moviesActions.loadingMovies());
const moviesResultAction = Observable.from(
moviesService.searchMovies(payload)
)
.map(moviesResultList => moviesActions.moviesLoaded(moviesResultList))
.catch(err => Observable.of(moviesActions.loadError(err)));
return Observable.concat(loadingAction, moviesResultAction);
});
}
const rootEpic = combineEpics(loadMoviesEpic);
export default rootEpic;
so basically this thing shall not be called, because the debounce time is 300ms and I'm trying to check the spy after 200ms. But after 10ms the spy is being invoked.
How can I properly test this epic? I accept any suggestion but preferably I would like to avoid marble testing and rely only on timers and fake timers.
Thanks :D

The issue is that debounce and debounceTime emit right away when they reach the end of the observable they are debouncing.
So since you are emitting with of, the end of the observable is reached and debounce allows the last emitted value through right away.
Here is a simple test that demonstrates the behavior:
import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
test('of', () => {
const spy = jest.fn();
of(1, 2, 3)
.pipe(debounceTime(1000000))
.subscribe(v => spy(v));
expect(spy).toHaveBeenCalledWith(3); // 3 is emitted immediately
})
To effectively test debounce or debounceTime you need to use an observable that keeps emitting and doesn't end using something like interval:
import { interval } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
test('interval', done => {
const spy = jest.fn();
interval(100)
.pipe(debounceTime(500))
.subscribe(v => spy(v));
setTimeout(() => {
expect(spy).not.toHaveBeenCalled(); // Success!
done();
}, 1000);
});

Instead of setTimeout try advanceTimersByTime

Related

Rxjs - cancel debounce in the specific case

Question about rxjs puzzle.
I have the input observable stream and it will emit after 3 secs when I type some.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
// input is HTMLInputElement
const input$ = fromEvent(input, "input");
input$
.pipe(debounce(() => interval(3000)))
.subscribe(e => console.log(e.target.value));
I would like to make a change to cancel the debounce and emit immediately once the button is clicked. But, if I don't click the button, it will wait 3 secs.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
const input$ = fromEvent(input, "input");
// add click observable stream
const click$ = fromEvent(button, "click");
input$
.pipe(debounce(() => interval(3000)))
// I can't get this to work in the mix!!
// .pipe(debounce(() => click$))
.subscribe(e => console.log(e.target.value));
How can this be achieved?
sounds like a race operator.
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
input$
.pipe(
switchMap(value => race(
click$,
timer(3000),
).pipe(
take(1),
mapTo(value),
)),
.subscribe(e => console.log(e.target.value));
Here is the solution to toggle debounce, what you have to do is to convert interval() to a stream that change interval time base on button click
Js
import { fromEvent, interval,timer} from 'rxjs';
import { debounce,scan,shareReplay,map,startWith,tap,switchMap} from 'rxjs/operators';
const input = fromEvent(document.getElementById('text'), 'input');
const debounceToggle=fromEvent(document.getElementById('toggle'),'click').pipe(
scan((acc,curr)=>!acc,false),
map(on=>on?0:3000),
startWith(3000),
shareReplay(1),
switchMap(value=>interval(value))
)
const result = input.pipe(debounce(() => {
return debounceToggle
}));
result.subscribe(x => console.log(x.target.value));
HTML
<button id="toggle">toggle debounce</button>
<input type="text" id="text"/>
Here could be another solution I think:
input$
.pipe(
debounce(
() => interval(3000).pipe(takeUntil(buttonClick$))
)
)
.subscribe(e => console.log(e.target.value));
debounce will emit the value that caused the inner observable's subscription, when it either completes/emits a value
// Called when the inner observable emits a value
// The inner obs. will complete after this as well
notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.emitValue();
}
// Called when the inner observable completes
notifyComplete(): void {
this.emitValue();
}
Source code
The following would be the simplest in my opinion:
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
merge(
input$.pipe(debounceTime(3000)),
click$
).pipe(
map(() => input.value)
).subscribe(val => console.log(val));
https://stackblitz.com/edit/rxjs-8bnhxd
Also, you are essentially "combining" 2 different events here, it doesn't make sense to me to rely on event.target.value, as it could be referring to different things which makes it hard to read.

Don't emit until some other observable has emitted, then emit all previous values

I'm looking for a way to buffer values of an observable until some other observable has emitted, but then emit all the previous values. Something like skipUntil, if skipUntil also emitted skipped values as soon as the second observable emitted.
--a--b----c-----d---e--- (source)
-----------1------------- (other1)
------------abc-d---e----(desired output)
You can use bufferWhen:
import { fromEvent, interval } from 'rxjs';
import { bufferWhen } from 'rxjs/operators';
const clicks = fromEvent(document, 'click');
const buffered = clicks.pipe(bufferWhen(() =>
interval(1000 + Math.random() * 4000)
));
buffered.subscribe(x => console.log(x));
Here's the custom operator I came up with. Not sure if it can be done in a prettier way.
export function bufferUntil(stopBufferSignal: Observable<any>) {
return <T>(source: Observable<T>): Observable<T> => {
return source.pipe(buffer(stopBufferSignal),
take(1),
flatMap(ar => {
const sourceWithNSkipped$: Observable<T> = source.pipe(skip(ar.length));
const bufferedItems$: Observable<T> = from(ar);
return bufferedItems$.pipe(concat(sourceWithNSkipped$))
}));
}
}

how to trigger something immediately then debounce

I have an observable bound to event keyUp on an input box.
For each key pressed. I want to console.log 'Do something now'.
And if there's no key pressed for 5 seconds, then I want to console.log 'Do something else'
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
const searchBox = document.getElementById('search');
const keyup$ = fromEvent(searchBox, 'keyup')
keyup$.pipe(
switchMap((i: any) => 'doSomethingNow'), // I use switchMap here because 'doSomethingNow' is an http request in my real code so that on each key pressed, it cancels the previous http request if it was not finished and start the new http request
debounceTime(2000),
map(_ => 'do something else')
)
.subscribe(console.log);
This code only print 'do something after debounce' after 5 seconds but never print 'domethingNow' after each key pressed
You can use the merge operator:
const searchBox = document.getElementById('search');
const keyup$ = fromEvent(searchBox, 'keyup');
const keyupEnd$ = keyup$.pipe(
switchMap(() => debounceTime(500))
);
const result = merge(
keyup$,
keyupEnd$
);
Have you tried something like:
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
var searchBox = document.getElementById('search');
var keyup$ = fromEvent(searchBox , 'keyup')
keyup$.pipe(
switchMap((i: any) => { console.log('do something');})
debounceTime(5000)) // with delay of 5 secs
.subscribe(console.log('do something else'););

Turn observable into subject

We have a function that gets a stream from the backend as observable. However we would like to be able to push to that observable as well to see the changes before those are done in the back-end. To do so I tried giving back a subject instead but the connection is still on going after the unsubscribe.
In other words, in the code below, we would like the console.log(i) not to start before we subscribe to the subject, and finish when we unsubscribe from it :
import { ReplaySubject, Observable, interval } from 'rxjs';
import { tap } from 'rxjs/operators'
function test() {
const obs = interval(1000).pipe(tap(i => console.log(i)));
const subj = new ReplaySubject(1);
obs.subscribe(subj);
return subj;
}
const subject = test();
subject.next('TEST');
const subscription = subject.pipe(
tap(i => console.log('from outside ' + i))
).subscribe()
setTimeout(_ => subscription.unsubscribe(), 5000);
example
You cannot subscribe in test. I guess you want to create an Observable and a Subject and merge those - you would have to return both separately.
return [subject, merge(subject, obs)]
and then
const [subject, obs] = test();
subject.next()
But I would do it by providing subject as a parameter.
import { ReplaySubject, Observable, interval, merge } from 'rxjs';
import { tap } from 'rxjs/operators'
function test(subject) {
return merge(
interval(1000).pipe(tap(i => console.log(i))),
subject
);
}
const subject = new ReplaySubject(1);
const obs = test(subject);
subject.next('TEST');
const subscription = obs.pipe(
tap(i => console.log('from outside ' + i))
).subscribe()
setTimeout(_ => subscription.unsubscribe(), 5000);

Redux Observables / RxJS: How to make epic that returns different actions based on if / else?

I am trying to hook up my app with in app purchases using this: https://github.com/chirag04/react-native-in-app-utils
I have an epic where I want to emit success if it succeeds, and failure if it fails. Something like this:
import 'rxjs';
import { InAppUtils } from 'NativeModules';
import * as packagesActions from '../ducks/packages';
import * as subscriptionActions from '../ducks/subscription';
export default function createSubscription(action$, store) {
return action$.ofType(packagesActions.SELECT)
.mergeMap(action => {
const productId = action.payload;
InAppUtils.purchaseProduct(productId, (error, response) => {
if(response && response.productIdentifier) {
return subscriptionActions.subscribeSuccess();
} else {
return subscriptionActions.subscribeFailure();
}
});
});
};
However I'm not sure how to write the contents of mergeMap. Is there a way to do this?
InAppUtils.purchaseProduct appears to use a Node-style callback. There is an RxJS static method that can be used to create an observable from such an API call: bindNodeCallback.
Within your mergeMap, you should be able to do something like this
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/bindNodeCallback';
import 'rxjs/add/observable/of';
...
.mergeMap(action => {
const productId = action.payload;
const bound = Observable.bindNodeCallback((callback) => InAppUtils.purchaseProduct(productId, callback));
return bound()
.map(response => subscriptionActions.subscribeSuccess())
.catch(error => Observable.of(subscriptionActions.subscribeFailure()));
});

Resources