1

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?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Josh Pittman
  • 7,024
  • 7
  • 38
  • 66

1 Answers1

3

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: {

      }
    }
  });
TameBadger
  • 1,580
  • 11
  • 15
  • You are a star. Thank you so much. I spent over an hour on this :( My current working theory is that the longer you spend looking for a bug the less likely you are to find it. – Josh Pittman Dec 05 '19 at 12:30