React: `react-test-renderer` and refs

Created on 15 Sep 2016  ·  17Comments  ·  Source: facebook/react

Do you want to request a _feature_ or report a _bug_?
Bug

What is the current behavior?
It's not possible to test component that use ref with the react-test-renderer utilitiesTesting: the refs are always null.

/* @flow */

import React from 'react';

export default class Foo extends React.Component {    
    /* the future refs */
    bar; 

    componentDidMount() {
        console.log(this.bar); // this.bar is null

        this.bar.doThings() // So this fail
    }

    render() {
        return (
            <div ref={(c) => { console.log('ref cb', c); this.bar = c; }}> {/* The callback is call but, `c` is null*/}
                <p>Hello World</p>
            </div>
        );
    }
}
import React from 'react';
import renderer from 'react-test-renderer';

it('should have valide ref', () => {
    const foo = renderer.create(<Foo />);

    expect(foo.toJSON()).toMatchSnapshot();
});

What is the expected behavior?

The ref should be usable.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

Only tested with these versions.

Most helpful comment

Thanks for taking the time to file an issue!

This is not a bug. react-test-renderer doesn’t _actually_ use the browser (or jsdom), so there are no real <div>s we could give you.

If your components crash expecting some methods (like focus()), a workaround was implemented in #7649 and will be part of 15.4.0. It will work like this:

import React from 'react';
import renderer from 'react-test-renderer';

function createNodeMock() {
  // You can return anything from this function.
  // For example:
  return {
    focus() 
      // Do nothing
    }
  }
}

it('should have valid ref', () => {
    const foo = renderer.create(<Foo />, {createNodeMock});
    expect(foo.toJSON()).toMatchSnapshot();
});

createNodeMock also accepts element as an argument so you can check element.type and return different mocks, say, for <div> and <input>.

I hope this helps!

I will close because we generally close issues that are fixed in master, and we think this is an adequate solution.

All 17 comments

Thanks for taking the time to file an issue!

This is not a bug. react-test-renderer doesn’t _actually_ use the browser (or jsdom), so there are no real <div>s we could give you.

If your components crash expecting some methods (like focus()), a workaround was implemented in #7649 and will be part of 15.4.0. It will work like this:

import React from 'react';
import renderer from 'react-test-renderer';

function createNodeMock() {
  // You can return anything from this function.
  // For example:
  return {
    focus() 
      // Do nothing
    }
  }
}

it('should have valid ref', () => {
    const foo = renderer.create(<Foo />, {createNodeMock});
    expect(foo.toJSON()).toMatchSnapshot();
});

createNodeMock also accepts element as an argument so you can check element.type and return different mocks, say, for <div> and <input>.

I hope this helps!

I will close because we generally close issues that are fixed in master, and we think this is an adequate solution.

A warning could be added if we use react-test-renderer it on a component that need refs.

Since test renderer renders deeply, I’m worried this might end up as a non-actionable warning soup. I think fixing it on a case by case basis with mocks might work out better.

For now I've disable the generation of the component by doing: jest.mock('./Foo');. I will give another try with react 15.4.0. Thanks for your quick reply.

@gaearon Can you recommend a workaround prior to the release of React 15.4? In my case, for instance, the component which uses ref is the one being tested, so mocking the entire component (via jest.mock) is not meaningful approach.

There is no workaround prior to its release because the workaround is added in that release. 😉
Can you try [email protected] with [email protected]?

Fair enough. I'll checkout the release candidate to make sure everything is working and then sit tight until 15.4 is released.

It also occurs to me that I could refactor the component such that imperative interactions associated with refs can be pulled out into a separate component which can then be mocked.

Hi, at start just want to say great work with createNodeMockto @Aweary :clap:

When testing some components i stumbled on some fishy behavior, basic everything works when on first render we have components rendered that has refs callbacks, but when on update we add new components with ref callbacks we got an error
TypeError: optionscreateNodeMock is not a function

dummy example

const B = React.createClass({
  onRefEl(ref) {
    console.log('ref el', ref);
  },
  _renderEl(i) {
    return (
      <span key={i} ref={this.onRefEl}>index: {i}</span>
    );
  },
  onRef(ref) {
    console.log('ref in B', ref);
  },
  render() {
    return (
      <div ref={this.onRef}>{this.props.array.map(this._renderEl)}</div>
    );
  }
});

const A = React.createClass({
  render() {
    return <div><B array={this.props.array}/></div>;
  }
});

function createNodeMock(element) {
  console.log('yee createNodeMock called');
  return '[ref object]';
}

it('dummy test', () => {
  const component = renderer.create(<A array={[0]}/>, {createNodeMock});
  let tree = component.toJSON();
  console.log('render 0', tree);

  component.update(<A array={[0, 1]}/>);
  console.log('render 1', tree);
  // TypeError: options.createNodeMock is not a function

  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

imho it's not correct behavior, after some digging i would say
https://github.com/facebook/react/blob/master/src/renderers/testing/ReactTestMount.js#L99-L121

change update to allow pass options maybe something like this:

ReactTestInstance.prototype.update = function (nextElement, options) {
  console.log("::my:update");
  invariant(this._component, "ReactTestRenderer: .update() can't be called after unmount.");
  var nextWrappedElement = React.createElement(TopLevelWrapper, { child: nextElement });
  var component = this._component;
  ReactUpdates.batchedUpdates(function () {
    var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(_assign({}, defaultTestOptions, options));
    transaction.perform(function () {
      ReactReconciler.receiveComponent(component, nextWrappedElement, transaction, emptyObject);
    });
    ReactUpdates.ReactReconcileTransaction.release(transaction);
  });
};

then in our test we can pass options, and we don't have TypeError :+1:

it('dummy test', () => {
  const component = renderer.create(<A array={[0]}/>, {createNodeMock});
  let tree = component.toJSON();
  console.log('render 0', tree);

  component.update(<A array={[0, 1]}/>, {createNodeMock});
  console.log('render 1', tree);
  // no TypeError! 

  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

but it's get more complicated if we want to simulate async update by setState, then also we are missing options so our dummy will look like:

// B component is the same as above...
const A = React.createClass({
  getInitialState() {
    return {
      array: []
    };
  },
  componentDidMount() {
    setTimeout(() => {
      try {
        this.setState({array: [0]});
      } catch (error) {
        // TypeError: options.createNodeMock is not a function
        console.log(error);
      }
    }, 300);
  },
  render() {
    console.log('render A');
    return <div><B array={this.state.array}/></div>;
  }
});
// createNodeMock is the same 
it('dummy test', (done) => {
  const component = renderer.create(<A/>, {createNodeMock});
  let tree = component.toJSON();
  console.log('render 0', tree);

  setTimeout(() => {
    console.log('render 1', tree);
    tree = component.toJSON();
    expect(tree).toMatchSnapshot();
    done();
  }, 600);
});

and the hard part basic from my understating ReactUpdatesFlushTransaction.getPooled(true) sets the ReactTestReconcileTransaction.testOptions to true

How the testOptions is stored... (hard to see that in quick dig in this code :dancer: maybe ReactUpdatesFlushTransactionis goo place to start?

Is this making sense at all ? or what do you think guys?

One more time awesome work with this :clap: @Aweary @gaearon

Let's reopen so we don't lose track of this.

@piecyk thanks for the details report, I'll look into this today and see what can be done 👍

I was using the RC release for this great createNodeMock feature, but also falled into this options.createNodeMock is not a function error. which only seem to happen in some cases?

simple example that breaks (i think it's even a different bug). tested on [email protected]

test("works", () => {
  class Foo extends React.Component {
    render () {
      return <div />;
    }
  }
  const inst = renderer.create(<Foo/>, { createNodeMock: () => "whatever" });
  inst.unmount();
});

test("doesnt", () => {
  class Foo extends React.Component {
    render () {
      return <div ref="foo" />;
    }
  }
  const inst = renderer.create(<Foo/>, { createNodeMock: () => "whatever" });
  inst.unmount();
});

will produce

  ● doesnt

    TypeError: Cannot read property 'getTestOptions' of undefined

      at ReactTestComponent.getPublicInstance (node_modules/react-test-renderer/lib/ReactTestRenderer.js:69:30)
      at Object.removeComponentAsRefFrom (node_modules/react-test-renderer/lib/ReactOwner.js:86:76)
      at detachRef (node_modules/react-test-renderer/lib/ReactRef.js:32:16)
      at Object.<anonymous>.ReactRef.detachRefs (node_modules/react-test-renderer/lib/ReactRef.js:84:5)
      at Object.unmountComponent (node_modules/react-test-renderer/lib/ReactReconciler.js:78:14)
      at ReactCompositeComponentWrapper.unmountComponent (node_modules/react-test-renderer/lib/ReactCompositeComponent.js:418:23)
      at Object.unmountComponent (node_modules/react-test-renderer/lib/ReactReconciler.js:79:22)
      at ReactCompositeComponentWrapper.unmountComponent (node_modules/react-test-renderer/lib/ReactCompositeComponent.js:418:23)
      at Object.unmountComponent (node_modules/react-test-renderer/lib/ReactReconciler.js:79:22)
      at node_modules/react-test-renderer/lib/ReactTestMount.js:97:23
      at ReactTestReconcileTransaction.perform (node_modules/react-test-renderer/lib/Transaction.js:140:20)
      at node_modules/react-test-renderer/lib/ReactTestMount.js:96:17
      at ReactDefaultBatchingStrategyTransaction.perform (node_modules/react-test-renderer/lib/Transaction.js:140:20)
      at Object.batchedUpdates (node_modules/react-test-renderer/lib/ReactDefaultBatchingStrategy.js:62:26)
      at Object.batchedUpdates (node_modules/react-test-renderer/lib/ReactUpdates.js:97:27)
      at ReactTestInstance.Object.<anonymous>.ReactTestInstance.unmount (node_modules/react-test-renderer/lib/ReactTestMount.js:94:16)
      at Object.<anonymous>._react2.default.Component (src/GL/gl-react-headless/tests/all.test.js:53:8)
      at process._tickCallback (internal/process/next_tick.js:103:7)

  ✓ works (8ms)
  ✕ doesnt (4ms)

what's weird is I have refs working in some cases (like most of my usecases) but there are specific cases that definitely break. Let me know if you want me to do more investigation.

do you guys plan to fix this for 15.4.0 ? This bug makes createNodeMock very unreliable when you write more advanced tests..

I unfortunately haven't had a lot of time to work on React lately, I can try to make an effort tonight after work to fix it.

I will try to have another look on that...
if some is starting with it, most simple test that we want to resolve is below
( add it at end of suit ReactTestRenderer )

  it('dynamic ref', () => {
    const onRef = jest.fn();

    class A extends React.Component {
      state = {a: []}; // but when on init we set state = {a: [1]}; it will pass
      componentDidMount() {
        this.setState({a: [1]});
      }
      renderChild(i) {
        return <div key={i} ref={onRef}>{i}</div>
      }
      render() {
        return <div>{this.state.a.map(this.renderChild)}</div>
      }
    }

    function createNodeMock(element) {
      return null;
    }

    ReactTestRenderer.create(<A/>, {createNodeMock});
    expect(onRef).toBeCalled();
  })

Thanks a lot for providing repro cases and to @Aweary for fixing this in #8261.

It works great! thanks you guys for fixing this.

Was this page helpful?
0 / 5 - 0 ratings