enzyme
enzyme copied to clipboard
shallow doesn't work correctly with useState + React.memo
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 ( )
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’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 Oh, this is badly Anyway, thank you for help, I'll wait for updates)
I’ll 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()>
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
@a-b-h-i-97 always happy to get PRs that improve the docs :-)