If I have a saga with this form:
function * sagaWorker() {
yield put(START_ACTION)
yield take(WAIT_FOR_ACTION)
yield delay(100)
yield put(END_ACTION)
}
I can successfully test it using runSaga
like this:
step('saga passes the tests', async () => {
const channel = stdChannel()
const dispatched = []
const options = {
dispatch: action => dispatched.push(action),
getState: () => {},
channel
}
const task = runSaga(options, sagaWorker)
channel.put(WAIT_FOR_ACTION)
await task.toPromise()
expect(dispatched).to.deep.eql([START_ACTION, END_ACTION])
})
However, if I move the delay in front of the take:
function * sagaWorker() {
yield put(START_ACTION)
yield delay(100)
yield take(WAIT_FOR_ACTION)
yield put(END_ACTION)
}
Now the saga doesn't run to completion and times out - it gets to the take
but the action never arrives in the channel.
Is it possible to test it using this form? I suspect I can make it work by call
ing the delay
s rather than yield
ing them directly but I'd like to know how to make it work without doing that (if it's possible).
In your test, you would start a saga, intercept/resolve async effects with effectMiddlewares and assert on things like state updates to test integration between your saga and a store.
Step by Step approach Our sagas being generator functions always yield effects which are saga factory functions like takeEvery, put, call etc. We can test each yield statement one by one by calling them using next() and asserting the returned values to the expected effect.
put is dispatching a new action with the result from the previous yield. put is non- blocking. In this example, we call put with some action. put is a non-blocking effect creator, so it dispatches an action (could be an action that triggers some other saga), but the saga is not waiting for this action to finish.
Using Redux Saga to handle multiple async requests Redux Saga then takes care of the invocation and return the result to the generator. The same thing happens with the put method. Instead of dispatching an action inside the generator, put returns an object with instructions for the middleware to dispatch the action.
Using yield call(() => myPromiseyDelay(500))
won't save you here. There will still be nothing to notice the "lost" action at the time it's dispatched.
When you post your WAIT_FOR_ACTION
the saga is is in a yielded state on the yield delay
. There is no queue for actions here, so by the time you get to yield take(WAIT_FOR_ACTION)
, the WAIT_FOR_ACTION
action has long since been dispatched, unnoticed by any of the saga logic you presented above (there was no active take
to grab the action).
Consider setting up an actionChannel
to capture these unlistened-for actions. They'll be queued up in the channel, ready-to-consume, after the delay
has completed.
So, something like:
function * sagaWorker() {
const channel = yield actionChannel(WAIT_FOR_ACTION)
yield put(START_ACTION)
yield delay(100)
yield take(channel)
yield put(END_ACTION)
}
So putting this all together as non-pseudocode:
const {
runSaga,
stdChannel,
effects: {
take,
put,
actionChannel,
delay
}
} = window.ReduxSaga
const WAIT_FOR_ACTION = "WAIT_FOR_ACTION";
const START_ACTION = "START_ACTION";
const END_ACTION = "END_ACTION";
(async() => {
const channel = stdChannel();
const dispatched = [];
const options = {
dispatch: action => dispatched.push(action),
getState: () => {},
channel
};
const task = runSaga(options, sagaWorker);
channel.put({
type: WAIT_FOR_ACTION
});
await task.toPromise();
console.log(dispatched);
})();
function* sagaWorker() {
const channel = yield actionChannel(WAIT_FOR_ACTION);
yield put({
type: START_ACTION
});
yield delay(100);
yield take(channel);
yield put({
type: END_ACTION
});
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/redux-saga.umd.min.js"></script>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With