When using React 14 with current versions of jest and enzyme, it is impossible to find mounted elements with a given id.
It is possible to find mounted elements with a given id.
See attached repro repo
| library | version
| ------------------- | -------
| enzyme | 3.7.0
| react | 0.14.9
| react-dom | 0.14.9
| react-test-renderer |
| adapter (below) |
@ljharb could it be the id is being set on the component not in the rendered html of the component.
https://github.com/conartist6/enzyme-1907-repro/blob/master/src/__tests__/MyComponent.test.js#L7
Ie
class MyComponent extends React.Component {
render() {
return <div />
}
}
const wrapper = mount(<MyComponent id="foo" />);
wrapper.find('#foo');
// instead of
class MyComponent extends React.Component {
render() {
return <div className="foo"/>
}
}
const wrapper = mount(<MyComponent />);
wrapper.find('#foo');
@conartist6 there's 4 tests there. The first one is incorrect - if you expect 2, it will pass - because in mount, both the MyComponent, and the div it renders, match the #foo selector.
In the second test, that's a bug. I'd indeed expect it to have a length of 1.
In the third test, rendered is the button - so .is('#foo') is true, but finding looks in the children, and there's nothing in there that matches. Add an expect(rendered.is("#foo")).toBe(true); and change the expected length from 1 to 0, and it passes.
The fourth test is expected to fail :-)
@sstern6 the implementation in the repro uses {...this.props}, so i don't think that's the issue.
tl;dr: There's a bug with hostNodes here, and I'm looking into it.
OK, figured it out :-)
hostNodes filters, per the documentation. rendered in this case is a list of 1 - a custom component. .hostNodes() filters it down to an empty list. If instead you do rendered.find('#foo').hostNodes(), you correctly get a wrapper around one button node - and the test passes.
If there's documentation changes you think would make this more clear, a PR would be appreciated!
Would it not be possible to just do something slightly different between React and RN?
Anyway, it seems I may have been confused by two separate issues. I had tried putting hostNodes second, and writing my own filter function, but I'm also using jest-enzyme and in what I guess is an unrelated bug whenever a wrapper is created as a result of a filtering operation the matcher thinks it isn't a wrapper :'
OK figured that out. Sorry if I created any confusion, there were so many things going on that I couldn't keep straight what was what myself. I misunderstood the jest-enzyme matcher syntax. Sadly at the moment it chokes on this problem since it handles doing the find itself, giving the user no opportunity to do the hostNodes clarification, so I guess I'll go put in a PR there, at least if there's no chance of clearing up the ambiguity on the Enzyme end.
I'm open to suggestions to improve things in enzyme, as long as it isn't anything HTML-specific.
I don't understand. Shouldn't the selectors be html specific in HTML? I guess the way I see it is that the selectors should always select and operate on the primitives. In DOM React, the primitives are HTML elements. It doesn't make sense for me to simulate a click on something that isn't a DOM element, and or look for Components with a particular CSS class name.
In Native Components themselves are primitives (if I understand correctly, having never used RN). In a that situation the current behavior makes plenty of sense to me.
For now such a change would be breaking, so it would make sense to guard it behind an additional config parameter passed to Enzyme.configure. I'm pretty sure it would also be simple enough to read that parameter inside the find implementation and if it is set to just go ahead and return wrapper.hostNodes(). What I definitely don't know is how to automagically do this only if dom-based rendering is being done. Surely Enzyme would know, since it is in charge of doing the rendering.
Selectors aren't necessarily tied to html, just because that's where they got their inspiration from.
(Separately, I strongly discourage any use of simulate, because it doesn't actually simulate anything, it just invokes a prop function)
While it might make sense to have a global configure option for mount to automatically apply .hostNodes() to string selectors, I don't think it would make sense for shallow - and we try very hard to minimize the differences between the two APIs.
Why wouldn't it make sense for shallow?
A mount render of react web always ends up producing HTML elements; a shallow render often doesn't.
I don't see why that's problematic. There's good APIs for executing a query in that situation: find({prop: value}) and find(<Component prop={value} />).
Just spitballing here, could es6 template strings help? What is selectors, instead of just being strings could be something like {[Enzyme.selectorSymbol]: "dom", value: ... }. Such a selector object would obviously be a pain to construct, which is where template strings might help. It could look like:
import {sel, mount} from "enzyme";
const rendered = mount(<MyButton id="foo" />);
// note: this matcher name seems bad since it might not be an element that is found
expect(rendered).toContainMatchingElement(sel.dom`#id`);
Enzyme could then easily understand the users's intent, and the solution would be relatively elegant and compatible with shallow, mount, render, as well as matcher libraries.
I guess to me it seems like if you're using .find on a wrapper, you want to search the entire tree, not just DOM nodes. enzyme isn't a "HTML traversal of a react tree" tool, it's a "traversal of a react tree" tool.
I don't really care which way it goes, I just find it hard to believe that a testing library would be implemented in such a way as to make it impossible to identify things which are tagged for testing purposes. Is it just me? What are you doing in code you build that keeps you from running into this issue?
We use .hostNodes when we need to; but to be fair, we also rarely use .find with anything but a component reference or an HTML tag name.
I found the answer to one of my questions: how are other people not hitting this more often?
The answer is apparently that in React Native convention for identifying components for test is a testID prop which sets a data-testid attribute. I only learned this from the react-testing-library documentation, but it explains exactly how most people are avoiding impaling themselves on this sword.
@conartist6 is that a broader react native convention, or only a react-testing-library convention? I'd suggest not using "test IDs"; it's a brittle approach in practice.
They say they're following a convention set up by react-native-web. I'm not interested in it personally because I want my test identifiers to be able to have more than one segment, e.g. data-qa="button save-button save-task-button". I do plan to adopt RTL for the moment, but seeing some of the things it can't do has made me appreciate some of the things enzyme can. Maybe some day when I have more time I'll come back to this issue, as it's quite interesting.
@ljharb Also it seems to me that your point about simulate simply isn't true.
simulate ... doesn't actually simulate anything, it just invokes a prop function
Unless never versions of enzyme have changed from what I'm looking at, simulate uses React test utils simulate, which is exactly what I thought it would. It constructs a the same synthetic event you would normally get. Yes, if you have event handlers which you've attached directly to the DOM they won't get triggered, and I'm sure there are discrepancies around access to the native event from the synthetic one, but everything else will bubble correctly which is very different from just translating simulate('click') into a call to onClick.
Perhaps what you're saying is that in newer versions if a component instance is targeted click will just run onClick?
I’m reasonably confident that it doesn’t bubble the way you think, based on the hundreds of bug reports I’ve gotten about it. You’re right it uses the test renderer, and that it will construct a synthetic event for you (synthetic events are planned to be removed from react-dom in a future version entirely, ftr). However, the issue is that it doesn’t fully “simulate” the actual events in a browser. For example, simulating change won’t cause key events to be fired nor will it change the input value - it’s just not the right api for what people actually want to be testing.