I'm using xstate to implement a login flow.
I have a machine where the initialState invokes a Promise, and if it's rejected it will redirect to a state that has an entry
action. I would like to test that the action is called at the right time properly.
machine.ts
{
id: 'entrance',
initial: States.FETCHING_SESSION,
states: {
[States.FETCHING_SESSION]: {
invoke: {
src: 'fetchSession',
onError: {
target: States.LOGGED_OUT,
}
}
},
[States.LOGGED_OUT]: {
entry: ['navigateToLogin']
type: 'final'
},
}
}
machine.spec.ts
const mockFetchSession = jest.fn()
.mockRejectedValueOnce({ error: new Error('401 unauthorized') })
const mockNavigateToLogin = jest.fn()
const service = interpret(entranceMachine.withConfig({
services: {
fetchSession: mockFetchSession
},
actions: {
navigateToLogin: mockNavigateToLogin
}
}))
it('Goes to login page on fail', (done) => {
service.onTransition((state) => {
expect(state.matches(States.LOGGED_OUT))
expect(mockNavigateToLogin).toHaveBeenCalled() // <- this test case always fails
done()
})
service.start()
})
I managed to make it work in kinda of an ugly way by wrapping expect around a setTimout.
setTimeout(() => expect(mockNavigateToLogin).toHaveBeenCalled(), 100)
I wonder if there is a better option? Thanks
First, I suspect setTimeout just skips the expect..
I ran into the same issue using withConfig
. The actions at least, are not called at all even if they appear in machine.options.actions
.
My code looks like this:
const myAction = jest.fn();
const actions = { myAction };
const withCfg = machine.withConfig({
actions,
});
const spy = jest
.spyOn(actions, 'myAction')
.mockImplementation(() => myAction());
// ...
expect(spy).toHaveBeenCalledTimes(1);
Note that in application those actions are executed just fine