enzyme: React 16 fragments (rendering arrays and strings) unsupported

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

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 55
  • Comments: 32 (8 by maintainers)

Commits related to this issue

Most upvoted comments

Temporary hack that seems to work well enough:

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

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>');

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>

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

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

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

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.

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

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.

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

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.

Oops, sorry. Will totally do that!

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.

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