enzymejs / enzyme

JavaScript Testing utilities for React
https://enzymejs.github.io/enzyme/
MIT License
19.95k stars 2.01k forks source link

React 16 fragments (rendering arrays and strings) unsupported #1213

Open danactive opened 7 years ago

danactive commented 7 years ago

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

/node_modules/react-dom/cjs/react-dom-server.node.development.js:2776
    var tag = element.type.toLowerCase();
                          ^

TypeError: Cannot read property 'toLowerCase' of undefined

Failing test

import Adapter from 'enzyme-adapter-react-16';
import { configure, shallow } from 'enzyme';
import React from 'react';
import test from 'tape';

function Fragments() {
  return [
    <li key="A">First item</li>,
    <li key="B" id="item">Second item</li>,
    <li key="C">Third item</li>
  ];
}

test('One element parent', (assert) => {
  configure({ adapter: new Adapter() });

  const wrapper = shallow(<Fragments />);

  const actual = wrapper.html();
  const expected = undefined;
  assert.notEqual(actual, expected, 'Has ID selector');

  assert.end();
});

Passing test

import Adapter from 'enzyme-adapter-react-16';
import { configure, shallow } from 'enzyme';
import React from 'react';
import test from 'tape';

function NotFragments() {
  return (
    <ul>
      <li key="A">First item</li>,
      <li key="B" id="item">Second item</li>,
      <li key="C">Third item</li>
    </ul>
  );
}

test('One element parent', (assert) => {
  configure({ adapter: new Adapter() });

  const wrapper = shallow(<NotFragments />);

  const actual = wrapper.find('#item').is('li');
  const expected = true;
  assert.equal(actual, expected, 'Has ID selector');

  assert.end();
});

-=Dan=-

tkrotoff commented 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

c-emil commented 7 years ago

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).

echenley commented 7 years ago

Temporary hack that seems to work well enough:

const component = shallow(<FragmentComponent />)
const fragment = component.instance().render()
expect(shallow(<div>{fragment}</div>).getElement()).toMatchSnapshot()
golankiviti commented 6 years ago

any update on this issue? or should I just wrap it in a div?

ikhilko commented 6 years ago

React 16.2.0 arrived with new fragments syntax. Any progress here to support it by enzyme?

variousauthors commented 6 years ago

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.

variousauthors commented 6 years ago

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)
FabioAntunes commented 6 years ago

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>
kaitmore commented 6 years ago

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.

davidjb commented 6 years ago

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.

davidjb commented 6 years ago

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.

ljharb commented 6 years ago

This issue is about fragment support; could you file a new issue for error boundary support?

davidjb commented 6 years ago

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

ljharb commented 6 years ago

ah, fair point; returning children without passing it through React.Children.only indeed requires React Fragments.

tkrotoff commented 6 years ago

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>');
JakobJingleheimer commented 6 years ago

<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.

willmendesneto commented 6 years ago

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.

mmcgahan commented 6 years ago

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.

ljharb commented 6 years ago

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

gregnb commented 6 years ago

Looks like support for this just landed https://github.com/airbnb/enzyme/pull/1733 and now released :)

heathzipongo commented 6 years ago

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...

ljharb commented 6 years ago

@heathzipongo what version of enzyme and which react adapter are you using?

heathzipongo commented 6 years ago

enzyme@^3.3.0 & enzyme-adapter-react-16@^1.1.1

ljharb commented 6 years ago

@heathzipongo Fragment support was added to enzyme in v3.4. Try upgrading.

astorije commented 6 years ago

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! :)

ljharb commented 6 years ago

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

astorije commented 6 years ago

Oops, sorry. Will totally do that!

FDiskas commented 6 years ago

I'm using

    "enzyme": "^3.5.0",
    "enzyme-adapter-react-16": "^1.5.0",

same problem

bbthorntz commented 6 years ago

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.

ljharb commented 6 years ago

@bbthorntz this issue is about components rendering arrays and strings; the issue you're looking for is #1799, which is being worked on.

MartinCerny-awin commented 6 years ago

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>;
}
ljharb commented 6 years ago

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.