Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Tracked by #1553
danactive opened this issue Oct 3, 2017 · 32 comments
Open
Tracked by #1553

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

danactive opened this issue Oct 3, 2017 · 32 comments

Comments

@danactive
Copy link

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
Copy link

tkrotoff commented Oct 9, 2017

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

tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 9, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 9, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 9, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 9, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 9, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
tkrotoff added a commit to tkrotoff/react-form-with-constraints that referenced this issue Oct 10, 2017
Enzyme 3 fails with fragments, see enzymejs/enzyme#1213 (comment)
@c-emil
Copy link

c-emil commented Oct 19, 2017

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
Copy link

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
Copy link

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

mockdeep added a commit to mockdeep/questlog that referenced this issue Nov 23, 2017
This adds parent task bread crumbs to both the task show view and the
focus view.

**Notes**

- Because `ParentTaskBreadCrumbs` is a recursive component (container ->
  component -> container -> component) they needed to be co-located in
  the same file. Having two files that each import the other was
  throwing an error for me. The component was coming in `undefined`.
- Enzyme doesn't yet have support for "fragments", which were introduced
  in React 16, otherwise I'd have avoided the outer `span` in
  `ParentTaskBreadCrumbs` and just returned an array. I tried the hack
  mentioned in the linked thread, but it didn't work for me.
  enzymejs/enzyme#1213
@ikhilko
Copy link

ikhilko commented Nov 30, 2017

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

@variousauthors
Copy link

variousauthors commented Dec 1, 2017

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
Copy link

variousauthors commented Dec 1, 2017

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
Copy link

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
Copy link

kaitmore commented Jan 8, 2018

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
Copy link

davidjb commented Jan 31, 2018

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
Copy link

davidjb commented Jan 31, 2018

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
Copy link
Member

ljharb commented Jan 31, 2018

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

@davidjb
Copy link

davidjb commented Jan 31, 2018

@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 #1213 (comment).

@ljharb
Copy link
Member

ljharb commented Jan 31, 2018

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

@tkrotoff
Copy link

tkrotoff commented Feb 7, 2018

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

@willmendesneto
Copy link

willmendesneto commented Apr 6, 2018

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
Copy link

mmcgahan commented Jul 9, 2018

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
Copy link
Member

ljharb commented Aug 3, 2018

@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
Copy link

gregnb commented Aug 8, 2018

Looks like support for this just landed #1733 and now released :)

@heathzipongo
Copy link

heathzipongo commented Aug 23, 2018

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
Copy link
Member

ljharb commented Aug 24, 2018

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

@ljharb ljharb changed the title React 16 Fragments unsupported React 16 fragments (rendering arrays and strings) unsupported Aug 24, 2018
@heathzipongo
Copy link

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

@ljharb
Copy link
Member

ljharb commented Aug 25, 2018

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

@astorije

This comment has been minimized.

@ljharb

This comment has been minimized.

@astorije

This comment has been minimized.

@FDiskas

This comment has been minimized.

@bbthorntz

This comment has been minimized.

@ljharb

This comment has been minimized.

@MartinCerny-awin
Copy link

MartinCerny-awin commented Oct 26, 2018

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
Copy link
Member

ljharb commented Oct 26, 2018

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.

@ljharb ljharb reopened this Jun 18, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests