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.
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 createNodeMock
to @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 ReactUpdatesFlushTransaction
is 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.
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:createNodeMock
also acceptselement
as an argument so you can checkelement.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.