0

Suppose I have a (rather contrived) controller and view

const useController = () => {
  const setId = useSetRecoilState(idState)
  return {
    setId
  }
}

const MyComponent = () => {
  const { setId } = useController()
  return (
    <button onClick={() => setId(1)}>Click me!</button>
  )
}

What is the correct pattern for testing

describe('my component', () => {
  test.todo('when click button, state updated to 1')
})

This pattern comes up a lot for me when I have a component that sets state and I want to verify that the component is actually updating the state (say, via an <input> and that I haven't forgotten to add the recoil setter into the onChange event handler.

I can imagine rendering that component under test inside a custom wrapper that sets the whole state as the value of an INPUT or something and then in my test I get that INPUT's value and I have the state, but is there a better way?

I'm essentially trying to spy on the atom's setter I guess.

Edit To clarify, I'm following what even the recoil testing library says: if you're testing a hook that is just for one component, you should test them together. So my "unit tests" treat the unit as "controller-as-hook" and component (the view).

user1713450
  • 1,307
  • 7
  • 18

1 Answers1

0

You don't need to spy on anything. Just act on the component or hook via react-test-library and trigger an action with that. Then you can use recoils snapshot_UNSTABLE() to generate a new snapshot of the recoil state which then allows you to test the value of an atom, selector, etc. against a expected value, like so:

const initialSnapshot = snapshot_UNSTABLE();
expect(initialSnapshot.getLoadable(idState).valueOrThrow()).toBe(0);

act(() => {/* act on your component or hook to mutate the state */});

const nextSnapshot = snapshot_UNSTABLE();
expect(nextSnapshot.getLoadable(idState).valueOrThrow()).toBe(1);
Johannes Klauß
  • 10,676
  • 16
  • 68
  • 122
  • Hm, that does not seem to work. `snapshot_UNSTABLE` does not have the state as mutated by act. Instead, it has state as defined in the defaults and does not have the updated state. Indeed, I'm not sure how it could have the actual state, since it's not contained inside teh same `RecoilRoot` as my component. – user1713450 May 26 '21 at 00:01
  • If I do what you did, nextSpanshot.getLoadble(myselector) gives a completely from-scratch state. In reality, the selector I'm using is based off an atom that is nullable (and default `null`). The selector's `get` throws a custom error ('atom not set') if the atom is null. In `RecoilRoot` I initialize it as non-null and all my selectors work. Then when I do `snapshot_UNSTABLE().getLoadable(selector).valueOrThrow()` it throws my custom error as if I had initialized RecoilRoot from scratch. – user1713450 May 26 '21 at 01:54
  • Well, of course you have to share the same RecoilRoot in the test as the component does, otherwise this obviously won't work (as you already said yourself). – Johannes Klauß May 26 '21 at 07:05