Enzyme: Shallow Render has everything wrapped in `ForwardRef`

Created on 16 Jul 2019  Â·  10Comments  Â·  Source: enzymejs/enzyme

Current behavior

This is with react native. When I use shallow rendering with enzyme, all components seem to be wrapped in a forwardRef. For example, if I do this in a test

    const wrapper = shallow(
      <TestView/>
    );
    console.log(wrapper.debug())

where TestView is

export const TestView: FC<PropType> = (props: PropType) => {
  return (
    <Text>Hello World</Text>
  );
};

The resulting output is

 <ForwardRef(Text)>
    Hello World
 </ForwardRef(Text)>

The problem with this is that you cannot do wrapper.find("Text"), and instead have to do wrapper.find('ForwardRef(Text') which is a lot less clear and seems fragile.

Expected behavior

I would expect the component to be just

 <Text>
    Hello World
 </Text>

Your environment

React Native 0.59.5

API

  • [x] shallow
  • [ ] mount
  • [ ] render

Version

| library | version
| ------------------- | -------
| enzyme | 3.6.0
| react | 16.8.6
| react-dom | 16.0.6
| react-test-renderer | 16.8.6
| adapter (below) |

Adapter

  • [x] enzyme-adapter-react-16
  • [ ] enzyme-adapter-react-16.3
  • [ ] enzyme-adapter-react-16.2
  • [ ] enzyme-adapter-react-16.1
  • [ ] enzyme-adapter-react-15
  • [ ] enzyme-adapter-react-15.4
  • [ ] enzyme-adapter-react-14
  • [ ] enzyme-adapter-react-13
  • [ ] enzyme-adapter-react-helper
  • [ ] others ( )

Most helpful comment

@ljharb thank you! You just dropped the nugget of gold that I needed. In my example above,
wrapper.find("Text") doesn't work, but wrapper.find(Text) does. I should have read the docs here better: https://airbnb.io/enzyme/docs/api/selector.html.

All 10 comments

enzyme won't work properly with react-native without a react-native adapter. Follow #1436 for that.

I am using the Adapter! mount works properly, but shallow does not. Here is my test-setup.js file

import { JSDOM } from "jsdom";

const jsdom = new JSDOM("<!doctype html><html><body></body></html>");
const { window } = jsdom;

const copyProps = (src, target) => {
  const props = Object.getOwnPropertyNames(src)
    .filter(prop => typeof target[prop] === "undefined")
    .map(prop => Object.getOwnPropertyDescriptor(src, prop));

  console.log("Props are" + props)  
  Object.defineProperties(target, props);
};

global.window = window;
global.document = window.document;
global.navigator = {
  userAgent: "node.js"
};
copyProps(window, global);

import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

// Setup enzyme's react adapter
configure({ adapter: new Adapter() });

// Ignore React Web errors when using React Native
console.error = message => {
  return message;
};

What i mean is, those adapters are for react web. There isn’t an adapter for react native atm.

Thanks for the quick response. I think what is misleading to me is the documentation then?

https://airbnb.io/enzyme/docs/guides/react-native.html

"As of v0.18, React Native uses React as a dependency rather than a forked version of the library, which means it is now possible to use enzyme's shallow with React Native components".

But if the behavior I am seeing is actually what is happening to everyone else, isn't this statement not really true? I guess you technically can use it, but it won't actually produce output thats predictable or usable.

Should I make a pull request to the documentation to say that shallow won't produce output that actually matches your component structure and is therefore not really usable?

I suspect that at some point RN started using forwardRef in all their primitives, and that doesn’t play well with shallow.

If you shallow render your own component, it should certainly be usable - you should be using .find(Text) and not finding by display name anyways.

@ljharb thank you! You just dropped the nugget of gold that I needed. In my example above,
wrapper.find("Text") doesn't work, but wrapper.find(Text) does. I should have read the docs here better: https://airbnb.io/enzyme/docs/api/selector.html.

It’s likely that to “fix” this, RN itself would have to explicitly set the display name on their now-forward-reffed components.

We have similar problem when using mount. I fixed it this way:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { createSerializer } from 'enzyme-to-json';

// Configure enzyme to use adapter for React
Enzyme.configure({ adapter: new Adapter() });

// Omit ForwardRef component in jest snapshot
// Bug: https://github.com/enzymejs/enzyme/issues/2190
const omitForwardRefInTree = (node) => {
  if (node.type && node.type.startsWith('ForwardRef')) {
    // ForwardRef have no child only when node is processed via `shallow` function,
    // otherwise `mount` is used.
    if (!node.children || node.children.length === 0) {
      return node;
    }

    if (node.children && node.children.length === 1) {
      return node.children[0];
    }

    return node.children;
  }

  return node;
};

// Configure jest to use json serializer for snapshot creation
expect.addSnapshotSerializer(createSerializer({
  map: omitForwardRefInTree,
  mode: 'deep',
  noKey: true,
}));

I use enzyme-to-json to render snapshots and it allows to change rendering of specific node, so I can omit it to temporarily fix the problem.

Snapshot before fix:

<ForwardRef(withForwardedRef(s))
            beforeLabel={
              <Icon
                icon="remote-connection"
                size="medium"
              />
            }
            clickHandler={[Function]}
            disabled={false}
            id="header__remoteAccessDialogButton"
            label="Remote access"
            labelVisibility="desktop"
            priority="default"
            variant="secondary"
          >
            <button
              className="Button__root__3zrxR
        Button__priorityDefault__hcWO8
        Button__sizeMedium__168V1
        Button__variantSecondary__1bYwq


        Button__withLabelHiddenMobile__3WSDW"
              disabled={false}
              id="header__remoteAccessDialogButton"
              onClick={[Function]}
              title="Remote access"
              type="button"
            >
              <span
                className="Button__beforeLabel__2TFTl"
              >
                <svgr-mock
                  height={16}
                  width={16}
                />
              </span>
              <span
                className="Button__label__3YN6e"
                id="header__remoteAccessDialogButton__label"
              >
                Remote access
              </span>
            </button>
          </ForwardRef(withForwardedRef(s))>

Snapshot after fix:

<button
            className="Button__root__3zrxR
        Button__priorityDefault__hcWO8
        Button__sizeMedium__168V1
        Button__variantSecondary__1bYwq


        Button__withLabelHiddenMobile__3WSDW"
            disabled={false}
            id="header__remoteAccessDialogButton"
            onClick={[Function]}
            title="Remote access"
            type="button"
          >
            <span
              className="Button__beforeLabel__2TFTl"
            >
              <svgr-mock
                height={16}
                width={16}
              />
            </span>
            <span
              className="Button__label__3YN6e"
              id="header__remoteAccessDialogButton__label"
            >
              Remote access
            </span>
          </button>

After upgrading react-native to the 0.63.2, finding an element by testing id does not work anymore, because TouchableOpacity component is wrapped by ForwardRef and it returns two nodes now instead of one.
Is there any solution to find an element by testing ID that do not return this ForwardRef or do I need to handle it (generate a logic to filter it, add wrapper around TouchabeOpacity to avoid components that have forwardRef...)?

Now it is retuning two nodes when I run: wrapper.find(`[data-ref="thumbsUp"]`) and use mount

// First node with the `data-ref = thumbsUp` 
<ForwardRef data-ref="thumbsUp" onPress={[Function: onPress]}>
     <TouchableOpacity data-ref="thumbsUp" onPress={[Function: onPress]} hostRef={{...}}>
     </TouchableOpacity> 
</ForwardRef>

// Second node with the `data-ref = thumbsUp`
<TouchableOpacity data-ref="thumbsup" onPress={[Function: onPress]} hostRef={{...}}>
</TouchableOpacity> 

I had the same problem after upgrading react-native version, and I can't do find(TouchableOpacity) because it would return more than 5 different items.

The solution I have figured out is to add the name of component (TouchableOpacity) in the find command to be more restricted. Do something like this:

wrapper.find(`TouchableOpacity[data-ref="idOfComponent"]`)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blainekasten picture blainekasten  Â·  3Comments

ahuth picture ahuth  Â·  3Comments

AdamYahid picture AdamYahid  Â·  3Comments

heikkimu picture heikkimu  Â·  3Comments

benadamstyles picture benadamstyles  Â·  3Comments