Search code examples
javascriptreduxredux-saga

Testing with runSaga with take and delay


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 calling the delays rather than yielding them directly but I'd like to know how to make it work without doing that (if it's possible).


Solution

  • 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>