3

I have a saga that listens for model updates and fires a save to network, debounced by 1 second:

export default function* graphModelUpdate() {
    const modelChangeDebounceTime = 1000;
    let task;

    while (true) {
        const action = yield take(IllustrationsActions.GRAPH_MODEL_CHANGED);
        if (task) {
            yield cancel(task);
        }
        task = yield fork(function* (payload) {
            yield call(delay, modelChangeDebounceTime);
            yield put(IllustrationsActions.apiUpdateGraph(payload));
        }, action.payload);
    }
}

Then I have this test, written in accordance with redux-saga docs and jest docs regarding timers, that tests the saga's effect rather than its implementation:

jest.useFakeTimers();

describe('graphModelUpdate saga', () => {
    let dispatched;
    let mockState;
    let subscribers = [];
    const emitAction = (action) => {
        subscribers.forEach(cb => cb(action));
    };
    const options = {
        subscribe: callback => {
            subscribers.push(callback);
            return () => subscribers = subscribers.filter(cb => cb !== callback);
        },
        dispatch: (action) => dispatched.push(action),
        getState: () => mockState
    };

    beforeEach(() => {
        dispatched = [];
        mockState = {};
    });

    it('should listen for IllustrationsActions.GRAPH_MODEL_CHANGED and dispatch IllustrationsActions.apiUpdateGraph, debounced by 1 second', () => {
        runSaga(options, graphModelUpdate);
        emitAction(IllustrationsActions.graphModelChanged('model one'));
        emitAction(IllustrationsActions.graphModelChanged('model two'));
        emitAction(IllustrationsActions.graphModelChanged('model three'));
        expect(dispatched).toEqual([]);
        jest.runTimersToTime(1500);
        expect(dispatched).toEqual([
            IllustrationsActions.apiUpdateGraph('model three')
        ]);
    });
});

The problem is that since the saga uses a fork that runs asynchronously, the final expect is executed before the debounced action is dispatched, no matter how much of the fake time passes (jest.runTimersToTime(1500)), making the test always fail.

How do I handle such case?

Dima Slivin
  • 599
  • 9
  • 19
  • Did you try [this SO answer](https://stackoverflow.com/a/46717241/3163075)? And did you try [this async example in jest](https://facebook.github.io/jest/docs/en/tutorial-async.html)? – Anima-t3d Mar 15 '18 at 09:16
  • [This SO answer](https://stackoverflow.com/a/43039182/3163075) might help as well. – Anima-t3d Mar 15 '18 at 09:20
  • 1
    Thanks, but they won't help much. I thought I might mock `delay` to have access to the promise that it returns (as it works by returning a promise and resolving it after the certain amount of time), but... that seems like a bad idea. Tomorrow the redux-saga guys change the `delay` implementation and my test won't be relevant any more. – Dima Slivin Mar 15 '18 at 11:35

1 Answers1

4

The fake timers correctly advance time, but it's still necessary to free the event loop so that the generator resumes execution, and only after we can proceed with the expectations

An example on how to do this without using setTimeout (since it's already mocked by jest):

jest.runTimersToTime(1500);
await Promise.resolve(); // generator resumes execution
expect(dispatched).toEqual([
   IllustrationsActions.apiUpdateGraph('model three')
]);