Enzyme: setState does not set state

Created on 27 Aug 2018  路  26Comments  路  Source: enzymejs/enzyme

Describe the bug
A clear and concise description of what the bug is.

I've setup versions enzyme 3.5.0 and enzyme-adapter-react-16 1.3.0.
I've created an enzyme test in this commit https://github.com/polkadot-js/apps/pull/265/commits/0d048c094b91762ac6a7812f5d4c5899b71b5af5 that mounts the component that passes. If I run a test with expect(wrapper.get(0)).toBe(1); it will show me that the component includes the correct props along with provided fixtures <Signer queue={[{"id": "test", "rpc": {"isSigned": true}, "status": "test"}]} t={[Function mockT]} />.
So far the test I've written that checks expect(wrapper).toHaveLength(1); is passing successfully.
However, I want to run a test to check that the <Modal className='ui--signer-Signer' ... (see https://github.com/polkadot-js/apps/blob/master/packages/ui-signer/src/Modal.tsx#L89) renders correctly, but it only renders when this.state.currentItem and this.state.currentItem.rpc.isSigned (see https://github.com/polkadot-js/apps/blob/master/packages/ui-signer/src/Modal.tsx#L83) are defined and true. So in the commit I created fixtures for the the currentItem state value, and wrote the following test to set the state with setState, update the component, and then check that the state has changed. But it doesn't appear to work, because the test results report that currentItem is still undefined instead of being the value I set to the fixtureCurrentItemState variable.

  it('sets the state of the component', () => {
    wrapper.setState({ currentItem: fixtureCurrentItemState });
    wrapper = wrapper.update(); // immutable usage
    expect(wrapper.state('currentItem')).toEqual(fixtureCurrentItemState);
    console.log(wrapper.debug());
  });

Note that I tried debugging with console.log(wrapper.debug()); and console.log(wrapper.html());, which I've used in the past without any issues, but neither of them output anything, so as an alternative I was able to check the state by running expect(wrapper.state()).toEqual(1);, which returned {"currentItem": undefined, "password": "", "unlockError": null}

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://github.com/polkadot-js/apps
  2. Clone Pull Request https://github.com/polkadot-js/apps/pull/265
  3. Install dependencies and run the tests with yarn; yarn run build; yarn run test;
  4. Remove .skip from the above mentioned tests
  5. See the failing test

Expected behavior
I expected setState to set the state

Screenshots
Code used screenshot:
screen shot 2018-08-27 at 9 36 33 am

Failing test screenshot:
screen shot 2018-08-27 at 9 38 47 am

Desktop (please complete the following information):

  • OS: macOS
  • Browser: Chrome
  • Version 68.0.3440.106

Most helpful comment

Yeah I seem to be running into this same issue. I have an instance method that sets state, so I run the instance method and ensure the the correct state gets set:

wrapper.instance().reviewApp({ uid: 'some_id' });
expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
expect(wrapper.state('adcOpen')).toBeTruthy(); //is false

Enzyme 3.5.0 enzyme-adapter-react-16 1.3.1

I changed it to manually call setState on the wrapper, but the state still isn't set.

wrapper.setState({adcOpen: true});
expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
expect(wrapper.state('adcOpen')).toBeTruthy(); //Still false

This still doesn't set the state correctly. I even tried it with the callback on setState and still no-go.

wrapper.setState({adcOpen: true}, () => {
  expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
  expect(wrapper.state('adcOpen')).toBeTruthy(); //Still false
});

EDIT: Also, I'm on React 16.4.2

All 26 comments

fwiw, wrapper = wrapper.update(); isn't required; "immutable" just means you have to re-find from the root.

setState is async; what happens if you do:

wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
  wrapper.update();
  expect(wrapper.state('currentItem')).toEqual(fixtureCurrentItemState);
  console.log(wrapper.debug());
});

Thanks. I tried that before and I just tried it again but it doesn't resolve the issue. I'll keep trying...

@ljharb @jacogr I tried changing the code to the following, using callbacks, promises, and async await, but the state of currentItem never changes from being undefined:

  it('set the state of the component using callbacks', (done) => {
    wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
      expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
      console.log(wrapper.debug());
      done();
    });
  });

  it('set the state of the component using promises', () => {
    Promise.resolve(wrapper.setState({ currentItem: fixtureCurrentItemState }))
      .then(_ => expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState))
      .catch((error) => console.log('error', error));
  });

  it('set the state of the component using async await', async () => {
    try {
      await wrapper.setState({ currentItem: fixtureCurrentItemState });
      expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
    } catch (error) {
      console.error(error);
    }
  });

Can you downgrade either, or both, of enzyme or the react 16 adapter, and see if this is a regression?

@ljharb I tried again and it doesn't work with the following versions. I tried using Callbacks, Promises, and Async Await, but cannot change the state using the below code.

  • "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.0",
  • "enzyme": "^3.4.4", "enzyme-adapter-react-16": "^1.2.0",
  • "enzyme": "3.3.0", "enzyme-adapter-react-16": "1.1.1",
it('set the state of the component using callbacks and anonymous function', (done) => {
  wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
    expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
    expect(wrapper.update().find('.ui--signer-Signer')).toHaveLength(1);
    done();
  });
});

it('set the state of the component using callbacks and named functions', (done) => {
  const checkExpectation = (done) => {
    expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
    done();
  };

  const doAsyncAction = (callback, done) => {
    wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
      callback(done);
    });
  };

  doAsyncAction(checkExpectation, done);
});

it('set the state of the component using promises', () => {
  Promise.resolve(wrapper.setState({ currentItem: fixtureCurrentItemState }))
    .then(_ => {
      expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
      expect(wrapper.update().find('.ui--signer-Signer')).toHaveLength(1);
    })
    .catch((error) => console.log('error', error));
});

it('set the state of the component using async await', async () => {
  try {
    await wrapper.setState({ currentItem: fixtureCurrentItemState });

    expect(wrapper.update().state('currentItem')).toEqual(fixtureCurrentItemState);
    expect(wrapper.update().find('.ui--signer-Signer')).toHaveLength(1);
  } catch (error) {
    console.error(error);
  }
});

Yeah I seem to be running into this same issue. I have an instance method that sets state, so I run the instance method and ensure the the correct state gets set:

wrapper.instance().reviewApp({ uid: 'some_id' });
expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
expect(wrapper.state('adcOpen')).toBeTruthy(); //is false

Enzyme 3.5.0 enzyme-adapter-react-16 1.3.1

I changed it to manually call setState on the wrapper, but the state still isn't set.

wrapper.setState({adcOpen: true});
expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
expect(wrapper.state('adcOpen')).toBeTruthy(); //Still false

This still doesn't set the state correctly. I even tried it with the callback on setState and still no-go.

wrapper.setState({adcOpen: true}, () => {
  expect(History.push).toHaveBeenCalledWith('/mobile/apps/detail/some_id');
  expect(wrapper.state('adcOpen')).toBeTruthy(); //Still false
});

EDIT: Also, I'm on React 16.4.2

So downgrading back down to 3.3.0 works for me. So I think it's enzyme causing this?

@comfroels can you confirm which exact version it breaks on? does it work on v3.4.0?

@ltfschoen I'm trying to reproduce your error, I think what's happening in your test is that getDerivedStateFromProps is being called when state gets updated and is reseting the value of currentItem.

A shorter failing test would be

it('set the state of the component using callbacks and anonymous function', (done) => {
  class TestComponent extends React.PureComponent<Props, State> {
    constructor (props) {
      super(props)
      this.state = {};
    }

    static getDerivedStateFromProps(props, { currentItem }) {
      return {
        currentItem: null,
      };
    }

    render() {
      return (
        <div>Rendered</div>
      );
    }
  }

  const wrapper = mount(<TestComponent />, {})
  wrapper.setState({ currentItem: 'foo' }, () => {
    expect(wrapper.update().state('currentItem')).to.equal('foo');
    done();
  });
});

I believe this makes sense to fail (cc @ljharb ), I think what the test should do is instead update props.queue so that getDerivedStateFromProps returns currentItem correctly

In the shorter example, getDerivedStateFromProps definitely forces currentItem to always be null - unless React itself opts not to call getDerivedStateFromProps when the props haven't changed?

React calls getDerivedStateFromProps when a component receives either new props or state. In the original example, Signer has a getDerivedStateFromProps that assigns state.currentItem from a prop value.

Then given that, is this still an issue, or should it be closed?

I believe this can be closed, not sure about @comfroels issue

@ljharb @jgzuke I made changes to my code in the following commit and then built and run the tests with yarn run build; yarn run test

However I still have the following problems:

  • It displays the below output errors when I make changes to the code as shown in this commit https://github.com/polkadot-js/apps/commit/7f0406ee8716cee187bca8301eb5344ddf20d746.
  • I can't using Enzyme debugging, as nothing is output when I run the tests with console.log(wrapper.debug()); (which is included in my latest commit), it simply displays console.log packages/ui-signer/src/Modal.spec.js:45 and then a couple of blank lines

screen shot 2018-09-09 at 3 39 15 pm
screen shot 2018-09-09 at 3 39 29 pm
screen shot 2018-09-09 at 3 39 45 pm
screen shot 2018-09-09 at 3 39 56 pm
screen shot 2018-09-09 at 3 40 07 pm

@ltfschoen thanks; in the future please post copypasted text and not screenshots of code, which are much harder to read.

Separately, await wrapper.setState() is meaningless, since it (and setProps) doesn't return a promise. You have to use the callback.

@ltfschoen in your linked commit console.log(wrapper.debug()); will output blank lines since the component is returning null and so has no output, this is expected.

if (!currentItem || currentItem.rpc.isSigned !== true) {
  return null;
}

If you debug after setting the currentItem you instead get

<Modal className="ui--signer-Signer" dimmer="inverted" open={true} centered={true} closeOnDimmerClick={true} closeOnDocumentClick={false} eventPool="Modal">
  <Translate(Extrinsic) value={{...}}>
    <Translate(Unlock) autoFocus={true} error={{...}} onChange={[Function]} onKeyDown={[Function]} password="" value={{...}} tabIndex={1} />
  </Translate(Extrinsic)>
  <ModalActions>
    <ButtonGroup>
      <Button isNegative={true} onClick={[Function]} tabIndex={3} text="extrinsic.cancel" />
      <Translate(ButtonOr) />
      <div>
        <Button className="ui--signer-Signer-Submit" isPrimary={true} onClick={[Function]} tabIndex={2} text="extrinsic.signedSend" />
      </div>
    </ButtonGroup>
  </ModalActions>
</Modal>

Sorry, haven't responded to this yet, the component in question for me, doesn't use getDerivedStateFromProps at all. I'll be probably playing around more with this today. So I'll check enzyme 3.4 and see if I have the same issue. I was going to try to get us up to React 16.5 today

@jgzuke Thanks I've got debug working now too with the following code. This is a big step.

  it('creates the element', async () => {
    try {
      await wrapper.setState({
        currentItem: fixtureCurrentItemState,
        password: '123',
        unlockError: null
      });

      await wrapper.setProps({
        queue: fixtureQueueProp
      });

      wrapper.update();

      expect(wrapper).toHaveLength(1);

      console.log(wrapper.debug());
    } catch (error) {
      console.error(error);
    }
  });

A few notes from what I've found so far today:
Upgrading to enzyme-adapter-react-16 from 1.1.1 to 1.5.0 worked fine (this was without updating enzyme)

upgrading to enzyme 3.4.0 and using enzyme-adapter-react-16 1.5.0 causes the same issue I had above.

upgrading to enzyme 3.4.0 and using enzyme-adapter-react-16 1.1.1 seems to also cause the same issue.

This is all on React 16.4.2

Also, sorry I haven't shared much component code. This is a private repo, with an NDA on the code. So I'm trying to give enough detail to you all. Let me know if there are any other things I can help with.

@ltfschoen again, await is useless on setState and setProps, since neither returns a promise.

@ltfschoen is this issue resolved then?

@comfroels I think you might be having a separate issue, it would be awesome if you could pinpoint the bug to a failing test similar to those found in https://github.com/airbnb/enzyme/blob/master/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx, as it stands without seeing the component I don't have any other ideas what might be wrong

@jgzuke Yes, I've established why it wasn't setting state. I've changed my code to use callbacks with the following, but I'd like to refactor it to use promises instead.

  it('testing', (done) => {
    try {
      wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
        wrapper.setProps({
          queue: fixtureQueueProp
        }, () => {
          wrapper.update();
          expect(wrapper.state('currentItem')).toEqual(expectedNextCurrentItemState);
          expect(wrapper.find('.ui--signer-Signer')).toHaveLength(1);

          console.log(wrapper.debug());
          done();
        });
      });
    } catch (error) {
      console.error(error);
    }
  });

I'm now trying to use mount instead of shallow, so I've mocked i18n with the following to overcome error TypeError: Cannot read property 'options' of undefined

jest.mock('react-i18next', () => ({
  // this mock makes sure any components using the translate HoC receive the t function as a prop
  translate: () => Component => props => <Component t={k => k} {...props} />
}));

But now it's giving error TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined

Thanks for that link, it's awesome!

Yes, I do believe I have a bad pattern that I'm using, as upgrading to React 16.5 causes me a very similar issue. You can close this, thanks for all the help ladies/gents!

@ltfschoen don鈥檛 use try/catch on your code there, return a new Promise instead, that resolves on the callback:

return new Promise((resolve) => {
  wrapper.setState({ currentItem: fixtureCurrentItemState }, () => {
      wrapper.setProps({
        queue: fixtureQueueProp
      }, resolve);
  });
}).then(() => {
  wrapper.update();
  expect(wrapper.state('currentItem')).toEqual(expectedNextCurrentItemState);
  expect(wrapper.find('.ui--signer-Signer')).toHaveLength(1);
});

@comfroels thanks!

you can setState like this

  it('sets the state of the component', () => {
    const componentInstance = wrapper
      .childAt(0)
      .instance();
    componentInstance.setState({ currentItem: fixtureCurrentItemState });
    wrapper = wrapper.update(); 
    expect(wrapper.state('currentItem')).toEqual(fixtureCurrentItemState);
  });

will pass

see my gist

https://gist.github.com/elvisgiv/e9056bde1cfbf5bcfecdfb32c10577ea

it('checks state setting', async () => {
    const wrapper = shallow(<Link title="Click"/>);
    try {
        await wrapper.setState({value: 'I need to do something...'});
        const textInput = wrapper.find('TextInput').first();
        expect(textInput.prop('value')).toStrictEqual('I need to do something...');
        console.log(wrapper.debug());
    } catch (error) {
        console.error(error);
    }
});

This works for me

Was this page helpful?
0 / 5 - 0 ratings