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

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

Open
2 of 13 tasks
MellowoO opened this issue Jul 18, 2019 · 18 comments
Open
2 of 13 tasks

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

MellowoO opened this issue Jul 18, 2019 · 18 comments

Comments

@MellowoO
Copy link

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

  • 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

  • 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 ( )
@ljharb
Copy link
Member

ljharb commented Jul 19, 2019

Try upgrading react and react-dom to latest?

@MellowoO
Copy link
Author

@ljharb yes

@ljharb
Copy link
Member

ljharb commented Jul 19, 2019

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

@MellowoO
Copy link
Author

yes, of course

@MellowoO
Copy link
Author

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

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

@ljharb
Copy link
Member

ljharb commented Jul 22, 2019

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.

@MellowoO
Copy link
Author

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

@ljharb
Copy link
Member

ljharb commented Jul 22, 2019

I’ll keep this open, to track it.

@ljharb ljharb reopened this Jul 22, 2019
@ljharb ljharb changed the title shallow doesn't work correctly with React.memo shallow doesn't work correctly with useState + React.memo Jul 22, 2019
@oviava
Copy link

oviava commented Sep 25, 2019

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

@mrdave-dev
Copy link

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

@johan-smits
Copy link

Any solution for this test case?

@frayeralex
Copy link

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'

@mihanizm56
Copy link

@frayeralex
code should not depend on writing tests

@mihanizm56
Copy link

any updates on this issue ??

@ElsaOOo
Copy link

ElsaOOo commented Sep 2, 2020

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.

@sakharovsergey
Copy link

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

@shufflerAbhi
Copy link

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

@ljharb
Copy link
Member

ljharb commented Apr 15, 2021

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

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

10 participants