Enzyme: shallow doesn't work correctly with useState + React.memo

Created on 18 Jul 2019  路  16Comments  路  Source: enzymejs/enzyme

Current behavior

Hi all! I try to test my functional component, wrapped by memo.

TestButton.tsx

```import React, { memo, useState } from 'react'

function TestButton () {
const [open, setOpen] = useState(false)
const toggle = () => setOpen(!open)
return (
className={open && 'Active'}
onClick={toggle}>
test

)
}

export default memo(TestButton)


#### TestButton.test.tsx

import React from 'react'
import { shallow } from 'enzyme';
import TestButton from './TestButton';

describe('Test', () => {
it('after click, button should has Active className ', () => {
let component = shallow()
component.find("button").prop('onClick')()
expect(component.find("button").hasClass('Active')).toBeTruthy()
})
})
`` I expect, that test will pass, but it fails and i can not understand why. If I will remove memo wrapper it passed. Or if I wrap testing component withmountand after click makecomponent.update()` it will be passed too

Expected behavior

Test should be passed

Your environment

API

  • [x] shallow
  • [ ] mount
  • [ ] render

Version

| library | version
| ------------------- | -------
| enzyme | 3.1.0
| react | 16.8.0
| react-dom | 16.8.0
| react-test-renderer |
| adapter (below) |

Adapter

  • [x] enzyme-adapter-react-16
  • [ ] enzyme-adapter-react-16.3
  • [ ] enzyme-adapter-react-16.2
  • [ ] enzyme-adapter-react-16.1
  • [ ] enzyme-adapter-react-15
  • [ ] enzyme-adapter-react-15.4
  • [ ] enzyme-adapter-react-14
  • [ ] enzyme-adapter-react-13
  • [ ] enzyme-adapter-react-helper
  • [ ] others ( )
shallow bug help wanted

Most helpful comment

Any solution for this test case?

All 16 comments

Try upgrading react and react-dom to latest?

@ljharb yes

@MellowoO to clarify - you specified you're on 16.8.0, but there's a number of bugs in the early 16.8 releases. Can you upgrade to the latest react and react-dom, confirm exactly which versions you're on, and what behavior you're seeing?

yes, of course

"react": "^16.8.6",
"react-dom": "^16.8.6",

@ljharb
Unfortunately, upgrate to latest version didn't help. Same behavior

So, in this case, it seems the use of the useState hook combined with memo may be the issue. If you use a class component and setState, it will work.

This is likely a limitation in react鈥檚 shallow renderer, which enzyme uses. Since hooks give us no way to hook into them, there鈥檚 not really a way for enzyme to react to hook changes.

@ljharb
Oh, this is badly
Anyway, thank you for help, I'll wait for updates)

I鈥檒l keep this open, to track it.

Not sure if related but memo doesn't seem to work with mount.

const MyComponent = memo(({ children, condition }) => condition ? children : null );

When I use enzyme to test it:

const wrap1 = mount(<MyComponent condition={true}>{children}</MyComponent>; // test passes when asserting `children` is `!null`

const wrap2 = mount(<MyComponent condition={false}>{children}</MyComponent>; //test fails when asserting `children === null`



If I use it without memo than it's all good

React: 16.9.0
React-DOM: 16.9.0
Enzyme: 3.10.0
enzyme-adapter-react-16: 1.14.0

I had a similar issue you're facing. I'm shallow rendering a component with memoized components on the inside, and was not seeing the updates in the wrapper.debug() output. The problem seems pretty obvious once I figured it out.

React.memo is checking your props for equality before re-rendering (that's great, that's why we're using memo!). If the props don't change, the component won't update.

Example Component

const MyComponent = React.memo((props: { testProp: boolean }) => {
  const [myState, setMyState] = useState('')
  return <input value={myState} onChange={e => setMyState(e.target.value} />
})

Example Test

it('updates', () => {
  const wrapper = enzyme.shallow(<MyComponent testProp={true} />
  wrapper.simulate('change', { target: { value: 'abc' } })
  expect(wrapper.prop('value')).toEqual('abc') // FAIL
})

To recap: our props didn't change, so our component _should not_ re-render.

How do we solve this? Change the props!

Updated Test

it('updates', () => {
  const wrapper = enzyme.shallow(<MyComponent testProp={true} />
  wrapper.simulate('change', { target: { value: 'abc' } })
  wrapper.setProps({ testProp: false })
  expect(wrapper.prop('value')).toEqual('abc') // FAIL
})

This technique may not work for everyone; for example, if you're relying on your props to be a certain primitive value, you might not be able to change it. In my case, I set the same prop values, but re-created the prop objects so that a strict equality check on prop objects would equate to false.

const props = () => ({ testPropObject: { ...testFixture } })
props() === { ...props() } // false

Any solution for this test case?

Maybe just split exports
then you can avoid "React.memo()" versions in your tests

// Component.js
export const Component = (props) => { ... }

export default React.memo(Component)

// Component.test.js
import { Component } from './Component.js' 

// Container.js
import Component from './Component.js'

@frayeralex
code should not depend on writing tests

any updates on this issue ??

React.memo doesn't work with react react-dom react-test-render same minor version.

"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1"

// Header.js
import React, { memo, useState } from "react";

const Header = () => {
  const [value, setValue] = useState("");
  const handleInputChange = (e) => {
    setValue(e.target.value.trim());
  };
  return (
    <div>
      <input
        type="text"
        data-test="input"
        value={value}
        onChange={handleInputChange}
        name="todo-input"
      />
    </div>
  );
};
export default memo(Header);

// Header.test.js
it("Header 缁勪欢 input 妗嗗唴瀹癸紝褰撶敤鎴疯緭鍏ユ椂锛屼細璺熼殢鍙樺寲", () => {
  const wrapper = shallow(<Header />);
  wrapper.find("input[data-test='input']").simulate("change", {
    target: {
      value: "learn jest",
    },
  });
  const userInput = "learn jest";
  expect(wrapper.find("input[data-test='input']").prop("value")).toEqual(
    userInput
  );
});
expect(received).toEqual(expected) // deep equality

    Expected: "learn jest"
    Received: ""

      22 |   });
      23 |   const userInput = "learn jest";
    > 24 |   expect(wrapper.find("input[data-test='input']").prop("value")).toEqual(
         |                                                                  ^
      25 |     userInput
      26 |   );
      27 | });

      at Object.<anonymous> (src/containers/todo-list/__tests__/unit/Header.test.js:24:66)

any updates on this issue ??

But use mount, it works.

"jest": "^25.1.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"react": "16.12.0",
"react-dom": "16.12.0"

describe('test', () => {
    it('success suite', () => {
        const Component: React.FC = () => null;

        const $el = mount(
            <Component>
                <div id={'findMe'} />
            </Component>
        );

        expect($el.exists('#findMe')).toBeFalsy();       // passed
    });

    it('failure suite', () => {
        const Component: React.FC = React.memo(() => null);

        const $el = mount(
            <Component>
                <div id={'findMe'} />
            </Component>
        );

        expect($el.exists('#findMe')).toBeFalsy();       // fail
    });
});

debug output on failure suite:

    <Memo()>
      <div id="findMe" />
    </Memo()>
Was this page helpful?
0 / 5 - 0 ratings