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
Advertisement
Answer
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