Open danactive opened 7 years ago
Or more simply put:
test('React 16 - fragments', () => {
const Fragments = () => [
<li key="1">First item</li>,
<li key="2">Second item</li>,
<li key="3">Third item</li>
];
const fragments = shallow(<Fragments />);
console.log('fragments:', fragments.debug());
});
test('React 16 - no fragments', () => {
const noFragments = shallow(
<div>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</div>
);
console.log('noFragments:', noFragments.debug());
});
Output:
fragments: <undefined />
noFragments: <div>
<li>
First item
</li>
<li>
Second item
</li>
<li>
Third item
</li>
</div>
See #1178, #1149
I have very similar issue, also related to if component doesn't have a wrapping element. I'm using React 16.
const StepsComponent = function StepsComponent({ steps }) {
return steps.map(function stepsMap(step) {
if (step.src) {
return <img src={step.src} alt={step.alt} />;
}
return step;
});
};
const steps = [
'123',
'another simple string',
{ src: './src/to/img', alt: 'img alt' },
];
const wrapper = mount(<StepsComponent steps={steps} />);
// The following only returns the img elements, it doesn't return simple nodes, such as strings.
const elements = wrapper.children().getElements();
// Now if I try this for shallow rendering, it won't even proceed
const wrapper2 = shallow(<StepsComponent steps={steps} />);
// This throws the following error:
// ShallowWrapper::getNode() can only be called when wrapping one node
const elements2 = wrapper.children().getElements();
If I wrap my component into a div, such as:
const StepsComponent = function StepsComponent({ steps }) {
return (
<div>
{ steps.map(function stepsMap(step) {
if (step.src) {
return <img src={step.src} alt={step.alt} />;
}
return step;
}); }
</div>
);
};
Using shallow rendering on such component and then calling wrapper.children().getElements()
will output all children, however simple nodes are presented as null
(but at least it indicates there are such nodes).
Temporary hack that seems to work well enough:
const component = shallow(<FragmentComponent />)
const fragment = component.instance().render()
expect(shallow(<div>{fragment}</div>).getElement()).toMatchSnapshot()
any update on this issue? or should I just wrap it in a div?
React 16.2.0 arrived with new fragments syntax. Any progress here to support it by enzyme?
TL;DR for now you can use .at(0).find('div')
to search fragments, lists, or plain old JSX elements.
it('always works', () => {
const subject = shallow(<Component />)
expect(subject.at(0).find('div').length).toBe(3)
})
class Frag extends React.Component {
render() {
return [
<div key='1' >1</div>,
<div key='2' >2</div>,
<div key='3' >3</div>,
]
}
}
class List extends React.Component {
render() {
const list = [1, 2, 3].map(n => <div key={n} >{n}</div>)
return list
}
}
class Norm extends React.Component {
render() {
return <div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>
}
}
class NormList extends React.Component {
render() {
const list = [1, 2, 3].map(n => <div key={n}>{n}</div>)
return <div>
{list}
</div>
}
}
describe('find()', () => {
it('Frag finds the divs', () => {
const subject = shallow(<Frag />)
expect(subject.at(0).find('div').length).toBe(3)
})
it('List finds the divs', () => {
const subject = shallow(<List />)
expect(subject.at(0).find('div').length).toBe(3)
})
it('Norm finds the divs', () => {
const subject = shallow(<Norm />)
expect(subject.at(0).find('div').length).toBe(4)
})
it('NormList finds the divs', () => {
const subject = shallow(<NormList />)
expect(subject.at(0).find('div').length).toBe(4)
})
})
find()
✓ Frag finds the divs (2ms)
✓ List finds the divs (2ms)
✓ Norm finds the divs (1ms)
✓ NormList finds the divs (1ms)
describe('childen()', () => {
it('Frag finds the divs', () => {
const subject = shallow(<Frag />)
expect(subject.children().length).toBe(3)
})
it('List finds the divs', () => {
const subject = shallow(<List />)
expect(subject.children().length).toBe(3)
})
it('Norm finds the divs', () => {
const subject = shallow(<Norm />)
expect(subject.children().length).toBe(3)
})
it('NormList finds the divs', () => {
const subject = shallow(<NormList />)
expect(subject.children().find('div').length).toBe(3)
})
})
childen()
✕ Frag finds the divs (2ms)
✕ List finds the divs (1ms)
✓ Norm finds the divs
✓ NormList finds the divs (3ms)
describe('at(0).children().find()', () => {
it('Frag finds the divs', () => {
const subject = shallow(<Frag />)
expect(subject.at(0).children().find('div').length).toBe(3)
})
it('List finds the divs', () => {
const subject = shallow(<List />)
expect(subject.at(0).children().find('div').length).toBe(3)
})
it('Norm finds the divs', () => {
const subject = shallow(<Norm />)
expect(subject.at(0).children().find('div').length).toBe(3)
})
it('NormList finds the divs', () => {
const subject = shallow(<NormList />)
expect(subject.at(0).children().find('div').length).toBe(3)
})
})
at(0).children().find()
✕ Frag finds the divs (2ms)
✕ List finds the divs
✓ Norm finds the divs (2ms)
✓ NormList finds the divs (1ms)
For what it's worth, I think the strangest thing here is that at(0).find()
works but at(0).children().find()
doesn't.
This morning, while reinstating fragments in my code base and testing them, I found yet another case that requires a bit of thinkering.
class FragList extends React.Component {
render() {
const list = [1, 2, 3].map(n => <div key={n}>{n}</div>)
return [
<div role='List'>{list}</div>,
<div></div>
]
}
}
it('FragList should have 2 outer divs and 3 inner', () => {
const subject = shallow(<FragList />)
expect(subject.at(0).find('div').length).toBe(2 + 3)
})
it('find the 2 outer divs using the technique above', () => {
const subject = shallow(<FragList />)
expect(subject.at(0).find('div').length).toBe(2)
})
it('find the divs inside the List', () => {
const subject = shallow(<FragList />)
const outer = subject.at(0).find('[role="List"]')
expect(outer.find('div').length).toBe(1 + 3)
})
it('find the divs inside the List!', () => {
const subject = shallow(<FragList />)
const outer = subject.at(0).find('[role="List"]')
expect(outer.at(0).find('div').length).toBe(1 + 3)
// ~~~~~~~~
})
it('find the divs inside the List!!!', () => {
const subject = shallow(<FragList />)
const outer = subject.at(0).find('[role="List"]')
expect(outer.shallow().at(0).find('div').length).toBe(1 + 3)
// ~~~~~~~~~~~~~~~
})
it('this also works...', () => {
const subject = shallow(<FragList />)
const outer = subject.at(0).find('[role="List"]')
expect(outer.shallow().find('div').length).toBe(1 + 3)
// ~~~~~~~~~~~
})
✕ FragList should have 2 outer divs and 3 inner (66ms)
✓ find the 2 outer divs using the technique above (2ms)
✕ find the divs inside the List (1ms)
✕ find the divs inside the List! (3ms)
✓ find the divs inside the List!!! (2ms)
✓ this also works... (2ms)
I have came across another issue when using <Fragment />
only the first children is rendered:
Modal.component
<Fragment>
<ModalBackdrop show={this.props.show} />
<ModalWrap show={this.props.show} onClick={this.outerClick}>
<ModalDialog show={this.props.show}>
<ModalContent>{this.props.children}</ModalContent>
</ModalDialog>
</ModalWrap>
</Fragment>
Modal.test
it('shoud do something', () => {
const component = mount(
<Modal show>
<div>YOLO</div>
</Modal>);
console.log(component.debug());
expect(component.find(ModalBackdrop).exists()).toBeTruthy(); ✓
expect(component.find(ModalWrap).exists()).toBeTruthy(); ✕
expect(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy(); ✕
expect(component.find(ModalDialog).contains(ModalContent)).toBeTruthy(); ✕
});
console.log(component.debug())
<Modal show={true} serverRender={false} toggle={[Function]}>
<styled.div show={true}>
<div className="sc-bdVaJa ceVaxf" />
</styled.div>
</Modal>
Running the same tests but changing the fragment to a div:
Modal.component
- <Fragment>
+ <div>
<ModalBackdrop show={this.props.show} />
<ModalWrap show={this.props.show} onClick={this.outerClick}>
<ModalDialog show={this.props.show}>
<ModalContent>{this.props.children}</ModalContent>
</ModalDialog>
</ModalWrap>
- </Fragment>
+ </div>
Modal.test
it('shoud do something', () => {
const component = mount(
<Modal show>
<div>YOLO</div>
</Modal>);
console.log(component.debug());
expect(component.find(ModalBackdrop).exists()).toBeTruthy(); ✓
expect(component.find(ModalWrap).exists()).toBeTruthy(); ✓
expect(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy(); ✓
expect(component.find(ModalDialog).contains(ModalContent)).toBeTruthy(); ✓
});
And last but not least the output from debug
console.log(component.debug())
<Modal show={true} serverRender={false} toggle={[Function]}>
<div>
<styled.div show={true}>
<div className="sc-bdVaJa ceVaxf" />
</styled.div>
<styled.div show={true} onClick={[Function]}>
<div className="sc-iAyFgw cpEsdu" onClick={[Function]}>
<styled.div show={true}>
<div className="sc-kEYyzF gLtZUo">
<styled.div theme={{ ... }}>
<div className="sc-hMqMXs bjEXbE">
<div>
YOLO
</div>
</div>
</styled.div>
</div>
</styled.div>
</div>
</styled.div>
</div>
</Modal>
Having the same issue as @FabioAntunes. I can see the correct output with .debug(), but my tests are failing because the second child of <Fragment>
isn't rendered at all.
Much the same as others have mentioned, a rendering like:
class Admin extends Component {
render () {
return (
<React.Fragment>
<h2>User Management</h2>
<p>You can search by name, email or ID.</p>
</React.Fragment>
)
}
}
when mount()
is used, the .html()
and .text()
functions return only the content of the first element within the fragment, eg:
const admin = mount(<Admin />)
console.log(admin.text()) # Logs --> User Management
console.log(admin.html()) # Logs --> <h2>User Management</h2>
However, when shallow()
is used, the .html()
and .text()
functions return the full text and expected content of the complete Component. In both cases, .debug()
returns the expected output.
Separate to my comment above, following the example used in Error Boundaries which renders like so:
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
Rendering this.props.children
with shallow()
results in the element being <undefined />
as was shown above and subsequent expect statements fail. When rendered with mount()
, the component renders correctly.
This issue is about fragment support; could you file a new issue for error boundary support?
@ljharb Not the same thing? The ErrorBoundary is just an example -- the issue comes from the use of return this.props.children
when rendering a component. Here's a simpler example:
const Foo = function (props) {
return props.children
}
const eb = shallow(
<Foo>
<a className='dummy'>Link</a>
<a className='dummy'>Link 2</a>
</Foo>
)
# Calling .children() errors with ShallowWrapper::getNode() can only be called when wrapping one node
expect(eb.children()).toHaveLength(2)
More than happy to create a separate issue but it seems to be the same as https://github.com/airbnb/enzyme/issues/1213#issuecomment-337834920.
ah, fair point; returning children without passing it through React.Children.only
indeed requires React Fragments.
Best solution I've found so far: wrap fragments with <div>
for the tests only.
class MyComponent {
render() {
return (
<>
<div>Foo</div>
<div>Bar</div>
</>
);
}
}
class MyComponentEnzymeFix extends MyComponent {
render() {
return <div>{super.render()}</div>;
}
}
const wrapper = mount(<MyComponentEnzymeFix />); // Instead of mount(<MyComponent />)
// instead of .toEqual(' <div>Foo</div><div>Bar</div> ')
expect(wrapper.html()).toEqual('<div><div>Foo</div><div>Bar</div></div>');
<React.Fragment>
as a wrapper is working for me (React 16.2.0 & Enzyme 3.3.0):
const wrapper = shallow(<MyComponent />);
console.log(
wrapper.debug(),
wrapper.children().debug(),
);
log output:
<Symbol(react.fragment)>
<PaymentIcon amount="$10.00" displayName="John Jingleheimer" type="card" size="huge" />
<Header as="h4" content={[undefined]} />
</Symbol(react.fragment)>
<PaymentIcon amount="$10.00" displayName="John Jingleheimer" type="card" size="huge" />
<Header as="h4" content={[undefined]} />
Which is exactly what I expect.
An option for now is
const TestWrapper = React.Fragment ? (
<React.Fragment>{this.props.children}</React.Fragment>
) : (
<div>{this.props.children}</div>
);
describe('if React.Fragment is available', () => {
before(() => {
Object.defineProperty(React, 'Fragment', {
configurable: true,
value: React.createClass({
render: function() {
return React.createElement(
'span',
{className: 'wrapper'},
Array.isArray(this.props.children) ?
this.props.children.map((el) => <span>{el}</span>) :
this.props.children
);
}
}),
});
});
after(() => {
Reflect.deleteProperty(React, 'Fragment');
});
it('should use React.Fragment component', () => {
const fragmentChildren = [
<p>Test123</p>,
<p>Test123</p>,
];
const component = mount(
<TestWrapper>
<fragmentChildren />
</TestWrapper>,
);
expect(component.find('span').is('span')).to.eql(true);
});
});
context('if React.Fragment is not available', () => {
it('should render a div', () => {
const component = mount(
<TestWrapper>
<p>Test123</p>
</TestWrapper>,
);
expect(component.find('div').is('div')).to.eql(true);
});
});
In that case, the span
tag is added, but we are covering the cases with React.Fragment
or not since TestWrapper
is respectively using the mocked React.Fragment when and if it exists. So your test is doing a proper assertion.
For anyone who finds this issue and is using enzyme-to-json
for Jest snapshots, it supports React.Fragment
as of version 3.3.0 - you may be able to fix some issues with fragment support by upgrading that package.
@danactive your original "failing test" will always fail - .html()
always returns a string, and never undefined
. Separately, the error you're getting is inside ReactDOMServer - can you confirm what versions of react and react-dom you're using?
Looks like support for this just landed https://github.com/airbnb/enzyme/pull/1733 and now released :)
If you do a wrapper.find(Fragment)
it fails with the following error:
TypeError: Enzyme::Selector expects a string, object, or Component Constructor
I would expect this to work properly...
@heathzipongo what version of enzyme and which react adapter are you using?
enzyme@^3.3.0
& enzyme-adapter-react-16@^1.1.1
@heathzipongo Fragment support was added to enzyme in v3.4. Try upgrading.
Hey @ljharb, using enzyme@3.5.0
and enzyme-adapter-react-16@1.3.0
(but also enzyme-matchers@6.0.4
and jest-enzyme@6.0.4
outside of this project, and TypeScript and ts-jest), I'm getting bitten by fragments as well and wondering if I'm doing something wrong.
Assuming the following code
const Foobar = () => (
<>
<div>Foo</div>
<div>Bar</div>
</>
);
And the following test:
describe('<Foobar />', () => {
test('should include Foo and Bar', () => {
const wrapper = mountWithIntl(<Foobar />);
expect(wrapper).toIncludeText('Foo');
expect(wrapper).toIncludeText('Bar');
});
});
Bar
simply does not get rendered:
FAIL test/src/Foobar.test.jsx
● <Foobar /> › should include Foo and Bar
Expected <IntlProvider> to contain "Bar" but it did not.
Expected HTML: "Bar"
Actual HTML: "Foo"
8 |
9 | expect(wrapper).toIncludeText('Foo');
> 10 | expect(wrapper).toIncludeText('Bar');
| ^
11 | });
12 | });
13 |
at Object.test (test/src/Foobar.test.tsx:10:21)
mountWithIntl
is a simple wrapper that adds an IntlProvider
(from react-intl
) to help us deal with i18n in our tests. If I remove it and replace with bare mount
, I get:
● <Foobar /> › should include Foo and Bar
Trying to get host node of an array
7 | const wrapper = mount(<Foobar />);
8 |
> 9 | expect(wrapper).toIncludeText('Foo');
| ^
10 | expect(wrapper).toIncludeText('Bar');
11 | });
12 | });
I get the same results if I swap the <>...</>
shorthand with <React.Fragment>...</React.Fragment>
.
However, if the code is:
const Foobar = () => (
<div>
<div>Foo</div>
<div>Bar</div>
</div>
);
then now the tests pass fine.
I'm very confused as Enzyme 3.5 is supposed to have fragment support. Do you see something just obviously wrong above?
One of my theories is that the TypeScript side of things (outside of this project, I know) would simply compile the fragment into an array and pass that to Enzyme, which then becomes https://github.com/airbnb/enzyme/issues/1178. I'm however more inclined to expect I did something wrong above than assume it's some other library's fault! :)
@astorije Note that <>
is React.Fragment
; whereas this issue is about "fragments" which is when components render arrays or strings.
Yes, v3.5 has Fragment support, so this seems like it should work. However, the use of enzyme-matchers
might be complicating things. Can you file a new issue (since this one isn't about <>
/Fragment
)?
Oops, sorry. Will totally do that!
I'm using
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.5.0",
same problem
What's the latest with this issue? We've still got the same issues using mount()
and <React.Fragment />
together. As discussed already, wrapper.debug()
is fine but wrapper.text()
and wrapper.html()
both only render the first child.
I don't think adding wrapping divs or extending components as suggested prior are acceptable solutions.
@bbthorntz this issue is about components rendering arrays and strings; the issue you're looking for is #1799, which is being worked on.
I am getting error when testing function as a child component with has React.Fragment
Test
const rerender = () => {
wrapper = shallow(outerWrapper.find('I18n').prop('children')());
};
describe('Component', () => {
beforeEach(() => {
outerWrapper = shallow(<Component />);
rerender();
});
it('renders', () => {
expect(outerWrapper).toHaveLength(1);
});
});
Component with error
render() {
return <I18n>{({ i18n }) => <React.Fragment />}</I18n>;
}
Error:
Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was symbol
.
Component without error:
render() {
return <I18n>{({ i18n }) => <div />}</I18n>;
}
You can shallow-render a component that renders a Fragment, but not a Fragment itself. Either way, that's unrelated to this issue, which is about rendering arrays and strings.
React 16 added a new return type of an array with many fragments of JSX, see React 16 blog post
Enzyme v3.0.0 with React 16 adapter 1.0.0 throws an error with this sample code
Error
Failing test
Passing test
-=Dan=-