I'm using jest
to test a redux-observable
epic that forks off an inner observable created using Observable.fromEvent
and listens for a specific keypress before emitting an action.
I'm struggling to test for when the inner Observable does not receive this specific keypress and therefore does not emit an action.
Using jest, the following times out:
import { Observable, Subject } from 'rxjs'
import { ActionsObservable } from 'redux-observable'
import keycode from 'keycode'
const closeOnEscKeyEpic = action$ =>
action$.ofType('LISTEN_FOR_ESC').switchMapTo(
Observable.fromEvent(document, 'keyup')
.first(event => keycode(event) === 'esc')
.mapTo({ type: 'ESC_PRESSED' })
)
const testEpic = ({ setup, test, expect }) =>
new Promise(resolve => {
const input$ = new Subject()
setup(new ActionsObservable(input$))
.toArray()
.subscribe(resolve)
test(input$)
}).then(expect)
// This times out
it('no action emitted if esc key is not pressed', () => {
expect.assertions(1)
return testEpic({
setup: input$ => closeOnEscKeyEpic(input$),
test: input$ => {
// start listening
input$.next({ type: 'LISTEN_FOR_ESC' })
// press the wrong keys
const event = new KeyboardEvent('keyup', {
keyCode: keycode('p'),
})
const event2 = new KeyboardEvent('keyup', {
keyCode: keycode('1'),
})
global.document.dispatchEvent(event)
global.document.dispatchEvent(event2)
// end test
input$.complete()
},
expect: actions => {
expect(actions).toEqual([])
},
})
})
My expectation was that calling input$.complete()
would cause the promise in testEpic
to resolve, but for this test it does not.
I feel like I'm missing something. Does anyone understand why this is not working?
I'm still new to Rx/RxJS, so my apologies if the terminology of this answer is off. I was able to reproduce your scenario, though.
The inner observable (
Observable.fromEvent
) is blocking the outer observable. The completed event on yourActionsObservable
doesn't propagate through until after the inner observable is completed.Try out the following code snippet with this test script:
Neither the
switchMapTo
marble diagram nor its textual documentation) clearly indicate what happens when the source observable completes before the inner observable. However, the above code snippet demonstrates exactly what you observed in the Jest test.I believe this answers your "why" question, but I'm not sure I have a clear solution for you. One option could be to hook in a cancellation action and use
takeUntil
on the inner observable. But, that might feel awkward if that's only ever used in your Jest test.I can see how this epic/pattern wouldn't be a problem in a real application as, commonly, epics are created and subscribed to once without ever being unsubscribed from. However, depending on the specific scenario (e.g. creating/destroying the store multiple times in a single application), I could see this leading to hung subscriptions and potential memory leaks. Good to keep in mind!