Enzyme: New "ref" style is not supported with mount()

Created on 6 Apr 2016  路  24Comments  路  Source: enzymejs/enzyme

It seems that wrapper.ref() does not work with the new "ref" style.

Here is a reproduction.

src/MyComponent.js

import React from 'react';

export class MyComponent extends React.Component {
  render() {
    return (
      <div>
        <Subcomponent 
          ref="oldStyle"
        />
        <Subcomponent 
          ref={(ref) => this.newStyle = ref}
        />
      </div>
    );
  }
}

export class Subcomponent extends React.Component {
  render() {
    return (
      <span>
        Hello
      </span>
    )
  }

The second test fails

import React from 'react';
import { mount, shallow } from 'enzyme';
import {MyComponent, Subcomponent} from '../src/MyComponent';
import { expect } from 'chai';

describe('<MyComponent />', () => {
  it('should find the old-style ref-ed component', () => {
    const wrapper = mount(<MyComponent />);
    expect(wrapper.ref('oldStyle').type()).to.equal(Subcomponent);
  });

  it('should find the new-style ref-ed component', () => {
    const wrapper = mount(<MyComponent />);
    expect(wrapper.ref('newStyle').type()).to.equal(Subcomponent);
  });
});

Looking at the react doc, they mention that the "'ref' callback will be executed immediately after the component is mounted". So maybe this bug is caused because enzyme does not go through all the full life-cycle events of the component.

Thanks!

Most helpful comment

Is there a solution to original issue posted by @jnak with new style refs? Seems this was hijacked about discussion of how to find DOM elements contained within a wrapped Modal. Original question refers more to how to access a member variable rather than a DOM element. Or is this still an unresolved bug?

All 24 comments

I am having a similar issue, I am showing a react-bootstrap modal and want to get at the ref which is rendered elsewhere.

const modal = home.find(Modal)
// afaik this is the way I have to access the ref on the instance?
var dialog = new ReactWrapper(modal.node._modal)

Then I get

ERROR: 'Warning: Failed propType: Required prop `Component` was not specified in `<<anonymous>>`.'
ERROR: 'Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).'
    Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. 

Any ideas would be useful :)

I figured this out.

import { Modal } from 'react-bootstrap'

const modal = home.find(Modal)
const instance = modal.node._modal
var dialog = new ReactWrapper(instance, instance)

If there are better ways to do this, would love to hear it. Also is this the correct way to create a new instance of ReactWrapper from a random react component?

@JakeGinnivan either shallow() or mount() around an element to wrap it?

@ljharb mount(modal.get(0)._modal) doesn't work

@JakeGinnivan if home is a wrapper, than home.find(Modal).mount() should give you a fresh wrapper, iirc

home is a wrapper, home.find(Modal) works fine, but modal creates a DOM element outside of the render tree then mounts it's children in that afaik.

At any rate, the children are not part of the tree under Modal. It stores a ref to the rendered react component on _modal. So I need to turn a plain react component into a wrapper. .mount() doesn't exist unless you extend the prototype of React.Component?

Just had the same issue with react-bootstrap/lib/Modal component. I added the following to my test setup based on what was suggested above by @JakeGinnivan and pretty much solved my problems.

ReactWrapper.prototype.findModal = function() {
  var wrapper = this.find(Modal);
  if (wrapper.length && wrapper.node._modal) {
    var modal = wrapper.node._modal;
    return new ReactWrapper(modal, modal);
  }
  return modal;
};

I've been having a nightmare with this problem in my own Modal implementation. I'm confused as to what ReactWrapper expects to be passed鈥攅verything I give it makes it throws an error.

I'm probably being thick but I still can't find a way to access the contents of the modal with enzyme.

const wrapper = mount(<MyModalContainer />);
const modal = wrapper.find(Modal);
console.log(modal.html())
console.log(modal.find('.thing-inside-modal'))

Have also tried this but html is always null and can't find contents:

const wrapper = mount(<MyModalContainer />);
const node = wrapper.find(Modal).node._modal;
const modal = new ReactWrapper(node, node);
console.log(modal.html())
console.log(modal.find('.thing-inside-modal'))

Thanks @jramirezl, it works!

const wrapper = mount(<MyModalContainer />)
const dialog = wrapper.find(Modal).getNode()._modal.getDialogElement()
const modal = new ReactWrapper(dialog, dialog)
console.log(modal.html())
console.log(modal.find('.thing-inside-modal'))

Is there a solution to original issue posted by @jnak with new style refs? Seems this was hijacked about discussion of how to find DOM elements contained within a wrapped Modal. Original question refers more to how to access a member variable rather than a DOM element. Or is this still an unresolved bug?

@draktheas wrapper.instance().memberProperty should work fine?

@ljharb Nice! This seems like _the_ solution.

@jnak I'm going to assume that the number of answers on this thread tells you how to do it.

In short, it's impossible for enzyme to declaratively know what a ref callback does, nor where it puts the ref - so .ref can only ever work for string refs.

I can't get the combination of mount/ref/memberProperty to work somehow.

// FileUpload.js
import React, { PureComponent} from 'react';
import PropTypes from 'prop-types';

export default class FileUpload extends PureComponent {
    static propTypes = {
        uploadFile: PropTypes.func.isRequired,
    };

    fileInput = null;

    uploadFile = () => {
        // At this point this.fileInput is undefined
        if (this.fileInput.files.length) {
            this.props.uploadFile(this.fileInput.files[0]);
        }
    };

    render() {
        return (
            <input
                type="file"
                ref={(elem) => { this.fileInput = elem; }}
                onChange={this.uploadFile}
            />
        );
    }
}



// FileUpload.test.js
import React from 'react';
import { mount, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import FileUpload from './FileUpload';

configure({ adapter: new Adapter() });

describe('FieldUpload', () => {
    it('calls uploadFile if a file was selected', () => {
        const uploadFile = jest.fn();
        const component = mount(
            <FileUpload uploadFile={uploadFile}/>
        );

        const file = 'test.zip';
        const changeEvent = {
            target: {
                files: [file]
            }
        };
        component.find('input[type="file"]').simulate('change', changeEvent);
        expect(uploadFile).toHaveBeenCalledWith(file);
    });
});

The test fails because the mock function is not called. Any ideas?

@timotgl yes, a few things. a) you can't mock out something via a prop that isn't a prop. b) don't ever use arrow functions in class methods. use a constructor-bound instance method, and then you can spy on FileUpload.prototype.uploadFile.

@ljharb
a) It is a prop, I didn't try to mock the instance method, just tried to make sure it calls through to this.props.uploadFile.
b) Thanks I'll try it this way. Apart from that, what's wrong with arrow functions in class methods?

@timotgl it's less performant and impossible to test.

@ljharb
If I want to use new ref style like @timotgl's code, should I stub all functions which contains the refs? or should I always stub the refs in test code? I think Enzyme have to treat the refs correctly :(

@ygnoh ref callbacks should work; you'd do wrapper.instance().property where your ref callback assigned to this.property. If that doesn't work for you (using mount), please file an issue

Faced some issue with new styled refs.

I'm using ref check to render node like

<div ref={(elem) => this.div = elem} />

{ this.div && this.props.showSmthAwesome && <AwesomeComponent />}

When I run enzyme.mount, looks like this.div remains undefined and it doesn't mount

Is it something known or a bug?

@MikeYermolayev try wrapper.forceUpdate(); the ref being set doesn't force a rerender.

@ljharb looks like forceUpdate is deprecated and currently it's just wrapper.update(). But still it hadn't helped much.

But in contrast everything works ok after I call wrapper.setProps. After that all refs are accessible in render method.

UPD: Oh, looks like it's connected with #1245

Was this page helpful?
0 / 5 - 0 ratings