enzyme
enzyme copied to clipboard
update() doesn't work for component conditional rendering (based on state)
Current behavior
I'm having a problem regarding a mounted component with conditional rendering based on state changes. Changing the state inside the test updates the state, but not what is rendered, even after invoking wrapper.update()
and wrapper.instance().forceUpdate()
.
Expected behavior
Changing the state inside the test context, and invoking one of (if not both) the wrapper.update()
or the wrapper.instance().forceUpdate()
should update the component and show the new rendering configuration, since it is based on the current state.
Your environment
API
- [ ] shallow
- [x] mount
- [ ] render
Version
library | version |
---|---|
enzyme | ^3.7.0 |
react | ^16.6.1 |
react-dom | ^16.6.1 |
adapter (below) | ^1.6.0 |
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 ( )
Can you provide a code example that demonstrates the problem? Also, what does calling wrapper.debug() output?
My speculation is that getNodesInternal in ReactWrapper.js isnt calling update on a state change, like Shallow is currently. A code snippet would help.
Sorry for the late reply.
Here's the test itself:
test('delete an account', async () => {
await mounted.setState({
activeAccount: {
id: 'test',
},
});
await mounted.instance().forceUpdate();
mounted.update();
expect(mounted.instance().state.activeAccount).toEqual({ id: 'test' });
const spy = jest.spyOn(mounted.instance(), 'deleteAccount');
const button = findByAttr(mounted, 'delete-button').first();
button.simulate('click', mockEvent);
expect(spy).toHaveBeenCalledTimes(1);
});
And here's the referenced part on the code itself:
{this.state.activeAccount ? (
<Button
enzyme-attr="delete-button"
className={`${styles.button} ${styles.delete}`}
onClick={() => this.deleteAccount()}
>
{'Delete'}
</Button>
) : null
}
The findByAttr
function is something I implemented to ease the search of an element on the DOM. It searches for the attribute enzyme-attr
.
Hope this helps.
thanks for the code snippet, per @minznerjosh rec, does wrapper.debug() output null? Ill have some time to look into this this week.
@sstern6 no, wrapper.debug()
(or, in my case, mounted.debug()
) returns the component rendered properly, but only for the initial state. After the state update, the component remains the same (without properly rendering the conditional part).
One thing that immediately jumps out to me is your use of async/await. The enzyme setState method does not return a Promise, so your use of await is effectively a noop. Try passing a callback as the second argument to setState instead.
@minznerjosh I know, that part of the code came from one of my desperate iterations 😛. I also tried having the remaining code as part of the callback for setState
, but that, too, didn't work...
@gilneto8 could you try it with shallow to see if it works? Also, findAttr is using enzymes .find or is this a custom built thing?
@sstern6 this is the findByAttr
implementation:
export const findByAttr = (wrapper, val) => wrapper.find(`[enzyme-attr='${val}']`);
I don't think shallow is usable here, because the component I'm testing is wrapped with a reducer, and on our architecture I still haven't found a way to export said component without that wrapper. Nevertheless, I'll try to find out a way. Thanks for the suggestion.
I'm experiencing this same issue. Mounted component that I'm calling setState
on, as well as update
and the mount never updates child component props that reference state. I'm currently unable to use shallow
for this particular test case. debug
returns the same component structure before and after calling setState
then update
.
thanks for the report, everyone.
@ljharb should this be a bug or an enhancement? Is wrapper.update() for mount "supposed" to propagate the change to children. The docs dont state that it will update the children of the wrapped component. This is related to another ticket im looking into so I dont mind looking into it. What do you think?
Thanks
I would expect that after wrapper.update()
, the entire tree would rerender based on the new props/state/etc - so the children should be updated.
If somebody was able to open up a PR with a failing testcase for this issue, that’d be amazing.
@minznerjosh ill do that and look into the issue as a whole.
From what i can tell its working as expected locally when writing the tests so decided not to push. Thinking the issue might be something else with the way the tests are written.
@twdrake-ap can you provide a code sample as well as the version of enzyme youre using?
Thanks
cc @minznerjosh
I think the issue here might be another case of not using root instances.
I had something I had something like this (simplified):
const wrapper = mount(
<MemoryRouter initialEntries={[{ pathname: "/", state: <some state> }]}>
<Route path="/" component={Page} />
</MemoryRouter>
).find(Page);
though you could simplify it further to a less meaningful example with React fragments (or anything really):
const wrapper = mount(
<>
<Page />
</>
).find(Page);
I then was using wrapper.update()
. Enzyme fails silently.
I think this use case is not supported unfortunately (at least in v3 anyway), but no error is thrown to indicate that. Maybe that's intentional and the reason isn't obvious to me.
@Bo98 that's a fair point; perhaps update
should throw when it's not called on a root wrapper - however, i'm not sure how many working tests that would break :-/
Yeah, that's a risk.
Maybe the only way would some sort of non-fatal warning. It would be better than nothing IMO until the opportunity arises that it can be raised to a fatal error.
I'm having this same problem.
Can find <CardBody />
, can't find conditionally rendered child button, even when state preconditions are met. Removing state precondition causes test to pass.
Adding update()
or .forceUpdate()
doesn't help.
"react": "16.8.6" "enzyme": "3.10.0" "enzyme-adapter-react-16": "1.14.0"
Test:
it('displays upload button on Card if file.status === Needs Input', () => {
const buttons = shallow(
<FileAccordion
files={[
{
originalFileData: {
path: 'Myfile.mp3',
fileStatus: 'Needs Input'
}
}
]}
/>
)
.find(Accordion)
.find(Card)
.find(Accordion.Collapse)
.find(Card.Body)
.find('button')
expect(buttons.length).toBe(1)
expect(buttons.at('0').text()).toBe('Start Upload')
})
Component:
// ...
<Card.Body>
{file.status === 'Needs Input' && (<button
onClick={(e) => {
// ...
}}
>
Start Upload
</button>)}
</Card.Body>
// ...
Hi @ljharb
I am facing this issue too. Conditional rendering is not working correctly, cannot find the correct node after doing a simple setProps.
"react": "^16.8.4" "enzyme": "^3.10.0" "enzyme-adapter-react-16": "^1.14.0"
I'm also facing this. I'm using a pattern for this component of mount
-> setImmediate
because there are fetch calls (mocked for the tests) which happen in componentDidMount
. What I notice is, for example, if I have the following div in my component:
<div className="breadcrumb-div">
<span>
<Link to="/app/apps/list">Connected Apps</Link>
</span>
{this.state.data_loaded && (
<span>
<span className="slash-span">/</span>
<Link
to={`/app/apps/listing/${this.props.match.params.integrationId}`}
>
{this.state.app_info!.listing.name}
</Link>
<span className="slash-span">/</span>
<span>{this.state.external_resource_name}</span>
</span>
)}
</div>
And inside the setImmediate
block of the test, I can run some logging and assertions:
const breadcrumbDiv = wrapper.find(".breadcrumb-div");
console.log(breadcrumbDiv.html());
expect(breadcrumbDiv.find('a[href="/app/apps/list"]').text()).toEqual(
"Connected Apps"
);
expect(
breadcrumbDiv.find('a[href="/app/apps/listing/123"]').text()
).toEqual("Typeform");
Which outputs the following:
console.log src/components/mappings/Mappings.test.tsx:122
<div class="breadcrumb-div"><span><a href="/app/apps/list">Connected Apps</a></span><span><span class="slash-span">/</span><a href="/app/apps/listing/123">Typeform</a><span class="slash-span">/</span><span>My Form</span></span></div>
● <Mappings /> › renders the breadcrumb correctly
Method “text” is meant to be run on 1 node. 0 found instead.
126 | // FIXME: Fails due to bug in enzyme: https://github.com/airbnb/enzyme/issues/1976
127 | expect(
> 128 | breadcrumbDiv.find('a[href="/app/apps/listing/123"]').text()
| ^
129 | ).toEqual("Typeform");
130 | done();
131 | });
at ReactWrapper.single (node_modules/enzyme/src/ReactWrapper.js:1166:13)
at ReactWrapper.text (node_modules/enzyme/src/ReactWrapper.js:629:12)
at Immediate.text (src/components/mappings/Mappings.test.tsx:128:57)
So html
is correct, and the first assertion (the link outside the conditional) works, but enzyme fails to find
any items inside the conditional. Methods like children
also don't show anything inside the conditional.
From communications with my coworkers who are doing similar testing, we believe this may be attributed to updates to components that are not the root component. For example, in the test above, I have this as the mounted component:
const wrapper = mount(
<MemoryRouter
initialEntries={[
"/app/apps/listing/123/mappings?connection_id=test-connection&external_resource_id=asdfg"
]}
>
<Route
exact
path="/app/apps/listing/:integrationId/mappings"
component={Mappings}
/>
</MemoryRouter>
);
My colleagues are reporting that they do not experience this issue when mounting their in-test component directly.
@shssoichiro try using the wrappingComponent
option for the MemoryRouter, instead of directly wrapping the Route with it.
I haven't faced the exact same issue, but similar ones. The problem in those cases has been the use of const instead of let on the wrapper I'm trying to update. Worth a try :)
the setState() in enzyme is sync method, you don't need update;just run it ;then you can get the new state and new render structure
this is a regular issue for me, i have heavily nested components (react-intl, styled-components-theme, redux providers) and so i will never get to a working state of a component without its wrappers. but i would love to see if my conditional rendered parts are working as expected in a test.
@Lelith I had a similar issue using redux and trying to force a render of conditional child components. My fix was to export my class component in addition to my connected component, like so:
// MyComponent.js
export class MyComponent extends Component {
...
}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
In my component, I'm exporting both the decorated (connected) component and the undecorated component for testing.
// MyComponent.test.js
import ConnectedMyComponent, { MyComponent } from './MyComponent'
describe('MyComponent', () => {
it('MyComponent conditional components are rendered', () => {
let wrapper = mount(<MyComponent {...props} />)
wrapper.setState({})
wrapper.update()
expect(wrapper.find('.whatever-youre-looking-for')).toHaveLength(1)
})
...
Therefore I can wrap the connected component in a <Provider>
while testing with redux and a mockStore, or just isolate what I need for passing props to my unconnected component and get the conditionally rendered child components.
Hope I'm not completely off-base and that this helps.
@escodel thank you for your reply :) it helps at least some of the components when i do the double export. then i can use shallow render method instead of mount / mount with providers. So i can at least add now tests to some of the parts.
What still bites me in the back is if i have either a styled component defintion inside one of them (because they need the theme provider to function) or using the react-intl api directly (also needs the provider). I guess my main goal will be to refactor those.