Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When multiple side-effects are used, testing machine logic using Jest fails #2920

Closed
mindy opened this issue Jan 6, 2022 · 2 comments
Closed

Comments

@mindy
Copy link

mindy commented Jan 6, 2022

Description

When mocking sendParent in Jest tests, and only if sendParent is called inline along with more than one side-effect, tests start to fail due to a type error.

For example, the following code:

...
success: {
  on: {
    BACK: {
      actions: [sendParent("BACK"), "trackEvent"],
    }
  }
}  
...

causes the following error when running Jest:

TypeError: Cannot read property 'type' of undefined

But having just one sendParent call does not cause an error: actions: sendParent("BACK")

And moving sendParent to "actions" also circumvents the error: actions: ["sendBackToParent", "trackEvent"]

Expected Result

Tests should pass regardless of how sendParent is called:

 PASS  src/childMachine.test.js
  ✓ starts machine in the "fetching" state (4 ms)
  ✓ notifies parent when "BACK" event occurs in "success" state (1 ms)

Actual Result

When calling sendParent with other side-effects, tests start to fail:

 FAIL  src/childMachine.test.js
  ✕ starts machine in the "fetching" state (1 ms)
  ✕ notifies parent when "BACK" event occurs in "success" state

  ● starts machine in the "fetching" state

    TypeError: Cannot read property 'type' of undefined

      13 | it('starts machine in the "fetching" state', () => {
      14 |   const machine = childMachine();
    > 15 |   const service = interpret(machine).start();
         |                                      ^
      16 |   expect(service.state.value).toEqual('fetching');
      17 | });
      18 |

      at toActionObject (node_modules/xstate/lib/actions.js:43:41)
      at node_modules/xstate/lib/actions.js:68:12
          at Array.map (<anonymous>)
      at Object.toActionObjects (node_modules/xstate/lib/actions.js:67:18)
      at StateNode.formatTransition (node_modules/xstate/lib/StateNode.js:1471:24)
      at node_modules/xstate/lib/StateNode.js:1546:22
          at Array.map (<anonymous>)
      at node_modules/xstate/lib/StateNode.js:1545:46
          at Array.map (<anonymous>)
      at StateNode.formatTransitions (node_modules/xstate/lib/StateNode.js:1544:278)
      at StateNode.get (node_modules/xstate/lib/StateNode.js:319:75)
      at StateNode.get (node_modules/xstate/lib/StateNode.js:297:30)
      at node_modules/xstate/lib/StateNode.js:207:24
          at Array.forEach (<anonymous>)
      at StateNode._init (node_modules/xstate/lib/StateNode.js:206:39)
      at StateNode.get (node_modules/xstate/lib/StateNode.js:1169:12)
      at node_modules/xstate/lib/interpreter.js:173:45
      at Object.provide (node_modules/xstate/lib/serviceScope.js:12:16)
      at Interpreter.get (node_modules/xstate/lib/interpreter.js:172:27)
      at Interpreter.start (node_modules/xstate/lib/interpreter.js:464:59)
      at Object.<anonymous> (src/childMachine.test.js:15:38)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

  ● notifies parent when "BACK" event occurs when in "success" state

    TypeError: Cannot read property 'type' of undefined

      22 |
      23 |   service.start('success');
    > 24 |   service.send('BACK');
         |           ^
      25 |
      26 |   expect(sendParent).toHaveBeenCalledWith('BACK');
      27 | });

      at toActionObject (node_modules/xstate/lib/actions.js:43:41)
      at node_modules/xstate/lib/actions.js:68:12
          at Array.map (<anonymous>)
      at Object.toActionObjects (node_modules/xstate/lib/actions.js:67:18)
      at StateNode.formatTransition (node_modules/xstate/lib/StateNode.js:1471:24)
      at node_modules/xstate/lib/StateNode.js:1546:22
          at Array.map (<anonymous>)
      at node_modules/xstate/lib/StateNode.js:1545:46
          at Array.map (<anonymous>)
      at StateNode.formatTransitions (node_modules/xstate/lib/StateNode.js:1544:278)
      at StateNode.get (node_modules/xstate/lib/StateNode.js:319:75)
      at StateNode.getCandidates (node_modules/xstate/lib/StateNode.js:331:27)
      at StateNode.next (node_modules/xstate/lib/StateNode.js:574:42)
      at StateNode.transitionLeafNode (node_modules/xstate/lib/StateNode.js:454:26)
      at StateNode._transition (node_modules/xstate/lib/StateNode.js:547:19)
      at StateNode.transition (node_modules/xstate/lib/StateNode.js:818:32)
      at node_modules/xstate/lib/interpreter.js:706:28
      at Object.provide (node_modules/xstate/lib/serviceScope.js:12:16)
      at Interpreter.nextState (node_modules/xstate/lib/interpreter.js:705:34)
      at node_modules/xstate/lib/interpreter.js:108:31
      at Scheduler.process (node_modules/xstate/lib/scheduler.js:69:7)
      at Scheduler.schedule (node_modules/xstate/lib/scheduler.js:48:10)
      at Interpreter.send (node_modules/xstate/lib/interpreter.js:104:23)
      at Object.<anonymous> (src/childMachine.test.js:24:11)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

Reproduction

https://codesandbox.io/s/adoring-hoover-fi3gw?file=/src/childMachine.js

Note: There's an issue with using jest.mock in CodeSandbox, so running the tests in CodeSandbox does not work.

@Andarist
Copy link
Member

Andarist commented Jan 6, 2022

The problem is that if the actions is an array then we assume that its items are valid and defined. I wasn't able to quite fix this in the provided repro - even though I believe that I've diagnosed the issue correctly...

If you mock sendParent like this:

  return {
    ...xstate,
    sendParent: jest.fn()
  };

then u effectively replace sendParent with a function that returns undefined and this is why the XState's code was tripping over this.

A better approach would be to mock it like this:

  const noop = () => {}
  return {
    ...xstate,
    sendParent: jest.fn().mockReturnValue(noop)
  };

This would make the sendParent return a noop function and functions are valid actions - so XState shouldn't trip over this.

For some weird reason, this didn't quite help but I believe this is some issue related to how this repo is setup with CRA (which isn't that helpful as CRA is a pretty standard setup). With the CRA's script (which is react-scripts test --env=jsdom) I was receiving a correct result from sendParent within childMachine.js file outside of the childMachine call. However, the same call was still returning undefined within that childMachine call - and I don't understand why. It looks like a possible transpilation issue or something. Note that I've ensured that both calls were executed in the very same instance of the file (I've created a random ID on the top of the file and logged stuff like:

console.log(id, sendParent())

Since the ID was the same... the sendParent should really point to the same function. However, in reality the executed code looks smth like:

console.log(id, _xstate.sendParent())

since the input ESM file is being transpiled to CJS with Babel so it can actually be executed by Jest. This kinda still doesn't add up though as usually if this would be wired incorrectly then I would expect one place to see the fixed mock and the other one to see the "real" sendParent. However, for some reason they both see mocks, just not the same mock (?).

On top of that... when I've bypassed CRA and executed this:

./node_modules/.bin/jest

then it worked as expected. So this has to me somehow related to the CRA setup as "raw" Jest handled this OK.

Note that to execute "raw" Jest I had to add a .babelrc file with such content:

{
    "presets": ["@babel/preset-env"]
}

I believe that the issue can be closed here since it's clearly not the issue with XState itself but rather with the mocking setup that you have used.

@mindy
Copy link
Author

mindy commented Jan 7, 2022

Thank you so much @Andarist! Changing the mock sendParent function as you suggested fixed the issue I was having.

I was receiving a correct result from sendParent within childMachine.js file outside of the childMachine call. However, the same call was still returning undefined within that childMachine call - and I don't understand why.

I also ran into this when I was writing tests -- glad to have more clarity on what was going on here.

Going to close out this ticket -- thanks again for all of your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants