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

Feature request: wrap JSX without any rendering #1444

Closed
dallonf opened this issue Dec 19, 2017 · 6 comments
Closed

Feature request: wrap JSX without any rendering #1444

dallonf opened this issue Dec 19, 2017 · 6 comments
Labels
feature request Issues asking for stuff that would be semver-minor

Comments

@dallonf
Copy link

dallonf commented Dec 19, 2017

Sometimes I find that I need to run assertions on raw ReactElements, without needing to shallow render or mount. This is especially common with the hip new "render prop" pattern. Here's an example based on React Router:

// Using Route's render prop to inject the `match`, as seen in this example:
// https://reacttraining.com/react-router/web/example/custom-link
const MyCustomLink = (children, to) => (
  <Route
    render={({ match }) => (
      <ObnoxiousHighlight on={Boolean(match)}>
        <Link to={to}>{children}</Link>
      </ObnoxiousHighlight>
    )}
  />
);

describe('MyCustomLink', () => {
  it('renders', () => {
    const outerWrapper = shallow(<MyCustomLink to="/home">Home</MyCustomLink>);
    const route = outerWrapper.find(Route);
    const innerJsx = route.prop('render')({ match: { __testMatch: true } });
    const innerWrapper = __someNewEnzymeFunction(innerJsx);
    expect(innerWrapper.find(ObnoxiousHighlight)).toHaveProp('on', true);
    expect(innerWrapper.find(Link)).toHaveProp('to', '/home');
  });
});

Note that if I called shallow(innerJsx), it would be treated as a shallow render of ObnoxiousHighlight, which isn't what I'm interested in testing in this suite.

As a workaround, I've been using this dirty function:

const wrap = element => shallow(<div>{element}</div>).childAt(0);

But it would be nice to have it as part of Enzyme.

@ljharb
Copy link
Member

ljharb commented Dec 20, 2017

Related to #692 (comment).

Here are the use cases as I understand them:

  • an element prop
  • a render function prop, that returns an element
  • potentially, a standalone element

And what is wanted is "pretend this element is what an SFC shallow-renders"?

An alternative workaround right now is shallow(() => element) or mount(() => element), which might be much cleaner than wrapping in a div, and might be much simpler than a new API method.

@ljharb ljharb added the feature request Issues asking for stuff that would be semver-minor label Dec 20, 2017
@dallonf
Copy link
Author

dallonf commented Dec 20, 2017

That is a good workaround, I'll have to try it.

I'd say the primary request is to be able to use Enzyme wrapper functions without modifying the input - sort of like a shallow render that's so shallow it has zero depth (i.e. doesn't render at all). The implementation details are less of a concern to me, but the current workarounds of adding an artificial level to prevent shallow rendering from going any deeper seem slightly wasteful.

@Heedster
Copy link

Heedster commented May 24, 2018

Just to clarify, just doing shallow(() => element) is not working for me. It gives me a
Invariant Violation: ReactShallowRenderer render(): Invalid component element. Instead of passing a component class, make sure to instantiate it by passing it to React.createElement.

shallow(React.createElement(() => element)) works. That is what you meant, right ? @ljharb

@ljharb
Copy link
Member

ljharb commented Jun 29, 2018

@Heedster ah, yes, good point

@ljharb
Copy link
Member

ljharb commented Jul 8, 2018

@dallonf it does seem like you could do this to cover your use case:

const RenderProp = route.prop('render');
const innerWrapper = shallow(<RenderProp match={{ __testMatch: true }} />);

Of the three cases above, this seems somewhat straightforward for the second.

I'm trying to weigh a few options:

  • a new entry point, alongside shallow/mount/render, that wraps a standalone element
  • an option to shallow that does the same
  • an instance method on ShallowWrapper that takes a prop name, and a list of arguments/props, and (if it's a function, invokes it first with the arguments and) wraps the element

In all cases, I'm not sure what to name it, nor am I sure that any of these are an ideal option.

@dallonf
Copy link
Author

dallonf commented Jul 8, 2018

The example you gave works in most scenarios, but I've seen some RPCs take a single argument as input instead of a props object, so you couldn't use the functions directly as components. In those cases, shallow(React.createElement(() =>render(/*input*/)) works fine - albeit a bit verbose.

Of the options you mentioned:

a new entry point, alongside shallow/mount/render, that wraps a standalone element

This is more or less how I treat my workaround function (which can be implemented as either element => shallow(<div>{element}</div>).childAt(0) or element => shallow(React.createElement(() => element)). For what it's worth, I call it wrap(), based on the convention of naming the result of shallow/mount/render wrapper.

an option to shallow that does the same

That could work as well! Although I'm also unsure what you might name it.

an instance method on ShallowWrapper that takes a prop name, and a list of arguments/props, and (if it's a function, invokes it first with the arguments and) wraps the element

My gut feeling is that this would be a bit too narrow of a solution. I've had use cases that wouldn't fit this solution well, like a renderCell/renderRow function intended for react-virtualized.

However, a slight variation on this idea where the children could be rendered in-place within the existing wrapper could be very helpful for the specific RPC scenario... let me doodle up some psuedocode to describe this:

const MyPage = ({ title }) => (
  <Route>
    {({ match }) => (
      <div>
        <h1>{title}</h1>
        <p>{match.url}</p>
      </div>
    )}
  </Route>
);

it('renders', () => {
  const wrapper = shallow(<MyComponent title="Hello world" />);
  // this is the new function
  wrapper.replaceChildren(
    // A selector for the element I want to replace
    Route,
    // a callback function for what to replace it with, first argument is the found element
    route => route.prop('children')({ match: { url: 'zombo.com' } })
  );
  //
  expect(wrapper).toMatchSnapshot();
  // `Route`'s un-snapshottable children have been replaced with something more representative
  // of how a developer thinks about render props
  /*
  <Route>
    <div>
      <h1>Hello world<</h1>
      <p>zombo.com</p>
    </div>
  </Route>
  */
});

This idea is very rough but hopefully I communicated the use case ok!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request Issues asking for stuff that would be semver-minor
Projects
None yet
Development

No branches or pull requests

3 participants