enzyme icon indicating copy to clipboard operation
enzyme copied to clipboard

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

Open MellowoO opened this issue 5 years ago • 18 comments

Current behavior

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

TestButton.tsx


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


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(<TestButton />)
    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 with mount and after click make component.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 ( )

MellowoO avatar Jul 18 '19 15:07 MellowoO

Try upgrading react and react-dom to latest?

ljharb avatar Jul 19 '19 06:07 ljharb

@ljharb yes

MellowoO avatar Jul 19 '19 13:07 MellowoO

@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?

ljharb avatar Jul 19 '19 21:07 ljharb

yes, of course

MellowoO avatar Jul 22 '19 07:07 MellowoO

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

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

MellowoO avatar Jul 22 '19 07:07 MellowoO

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’s shallow renderer, which enzyme uses. Since hooks give us no way to hook into them, there’s not really a way for enzyme to react to hook changes.

ljharb avatar Jul 22 '19 14:07 ljharb

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

MellowoO avatar Jul 22 '19 15:07 MellowoO

I’ll keep this open, to track it.

ljharb avatar Jul 22 '19 15:07 ljharb

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

oviava avatar Sep 25 '19 11:09 oviava

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

mrdave-dev avatar Jan 22 '20 23:01 mrdave-dev

Any solution for this test case?

johan-smits avatar Jan 28 '20 12:01 johan-smits

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 avatar Apr 15 '20 10:04 frayeralex

@frayeralex code should not depend on writing tests

mihanizm56 avatar Jul 19 '20 14:07 mihanizm56

any updates on this issue ??

mihanizm56 avatar Jul 19 '20 14:07 mihanizm56

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.

ElsaOOo avatar Sep 02 '20 00:09 ElsaOOo

"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()>

sakharovsergey avatar Dec 08 '20 15:12 sakharovsergey

It would be really nice to have this mentioned along with the couple of other issues mentioned in the introduction page. I spent a frustrating few hours last night without realizing that memo would be the culprit here :|

https://enzymejs.github.io/enzyme/#react-hooks-support image

shufflerAbhi avatar Apr 01 '21 10:04 shufflerAbhi

@a-b-h-i-97 always happy to get PRs that improve the docs :-)

ljharb avatar Apr 15 '21 23:04 ljharb