I am trying to test that a promise is invoked when a state transition happens.
I followed the approach outlined in the official xState tutorial but I get the following error
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout
This is my state machine, all it does is invokes a promise when you transition from the initial state.
export const statsMachine = Machine(
{
id: 'stats',
initial: 'incomplete',
states: {
incomplete: {
on: {
MODAL_OPENED: 'loading',
},
},
loading: {
invoke: {
id: 'setRatioDefaultsInFirebase',
src: (context, event) => setStatDefaults(event.payload && event.payload.userId),
onDone: {
target: 'modal',
},
onError: {
target: 'incomplete',
},
},
},
modal: {...}
}
})
This is my test. rather than firing a real api call like they do in the tutorial, I want to mock my api call. I'm using jest to mock the side effect. I want to assert that the mocked side effect was called. But I get the error out lined above.
jest.mock('../statsAPI');
test('stats should start off with minimum ratios', done => {
setStatDefaults.mockResolvedValueOnce();
const statsBoxService = interpret(statsMachine)
.onTransition(state => {
if (state.matches({ selected: 'modal' })) {
expect(setStatDefaults).toHaveBeenCalled();
done();
}
})
.start();
statsBoxService.send('MODAL_OPENED');
});
What do I have to change to assert that my mocked side effect got called when the machine transitioned?
I think it could be as simple as your if statement being wrong:
if (state.matches({ selected: 'modal' })) {
should be
if (state.matches('modal')) {
In the example, 'initial','loading','loaded','failed' are children of the state 'selected'
That being said, I played around with your example, and found this works, it's slightly different to your implementation in terms of what it mocks out:
machines.test.ts:
import { interpret } from 'xstate';
import { statsMachine } from './machines';
test('stats should start off with minimum ratios', done => {
global.fetch = jest.fn().mockImplementation(
() => Promise.resolve({ json: () => Promise.resolve({}) })
);
const statsBoxService = interpret(statsMachine)
.onTransition(state => {
if (state.matches('modal')) {
expect(global.fetch).toHaveBeenCalledTimes(1);
done();
}
})
.start();
statsBoxService.send('MODAL_OPENED');
});
machines.ts:
import { Machine } from 'xstate';
export const setStatDefaults = async (t: any) => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
return response.json();
};
export const statsMachine = Machine(
{
id: 'stats',
initial: 'init',
states: {
init: {
on: {
MODAL_OPENED: 'loading',
}
},
incomplete: {
on: {
MODAL_OPENED: 'loading',
}
},
loading: {
invoke: {
id: 'setRatioDefaultsInFirebase',
src: (context, event) => setStatDefaults(event.payload && event.payload.userId),
onDone: {
target: 'modal',
},
onError: {
target: 'incomplete',
},
},
},
modal: {
}
}
});