Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React components render correctly in browser, but Jest test errors when rendering: "Only a ReactOwner can have refs"

I have two components in React that render just fine and produce expected behavior in the browser, but can't seem to be rendered when running a test via Jest.

descriptions.js

var React = require('react/addons');
var $ = require('jquery');
var Description = require('./description.js');

var Descriptions = React.createClass({
    getInitialState: function () { //container always starts with at least one description field that is empty, or whatever is contained in props
        var descriptions = [];
        if (this.props.info == null) {
            descriptions.push({num: 0, data: ''});
        } else {
            $.each(this.props.info, function (i, string) {
                descriptions.push({num: i, data: string});
            });
            if (descriptions.length == 0) { //we want at least one description field at all times
                descriptions.push({num: 0, data: ''});
            }
        }
        return {descriptions: descriptions, focus: -1};
    },
    componentWillReceiveProps: function (nextProps) { //props are updated
        var descriptions = []; //we don't care about previous values, so we will make a new list
        $.each(nextProps.info, function (i, string) {
            descriptions.push({num: i, data: string});
        });
        if (descriptions.length == 0) { //we want at least one description field at all times
            descriptions.push({num: 0, data: ''});
        }
        this.setState({descriptions: descriptions});
    },
    addDescription: function (pos) { //adds a new description underneath the last one in the container
        var descriptions = this.state.descriptions;
        var max = 0;
        $.each(descriptions, function (i, item) {
            if (item.num > max) max = item.num;
        });
        descriptions.splice(pos + 1, 0, {num: max + 1, data: ''});
        this.setState({descriptions: descriptions, focus: pos + 1}); //focus the new description
    },
    removeDescription: function (pos) { //remove a description from the array given its array position
        var descriptions = this.state.descriptions;
        if (descriptions.length != 1) {
            descriptions.splice(pos, 1);
            this.setState({descriptions: descriptions, focus: pos == 0 ? 0 : pos - 1});
        }
    },
    render: function () {
        var items = this.state.descriptions.map(function (item, i) { //add one Description for every item in the array
            return (
                <Description key={item.num} addDescription={this.addDescription}
                             removeDescription={this.removeDescription}
                             descriptionNum={i} focus={i == this.state.focus} data={item.data}/>
            );
        }.bind(this));
        return (
            <div className="descriptions">
                {items}
            </div>
        );

    }
});

module.exports = Descriptions;

Essentially, this component is a container for one or more child "Description" components, and the amount of "Description" components that need to be rendered depends on the passed info prop, which holds an array of strings.

description.js

var React = require('react/addons');

var Description = React.createClass({
    mixins: [React.addons.LinkedStateMixin],
    componentDidMount: function () { //focus the input if it was added after page load
        if (this.props.focus) {
            this.refs.descInput.getDOMNode().focus();
        }
    },
    componentWillReceiveProps: function (nextProps) {
        if (nextProps.focus) {
            this.refs.descInput.getDOMNode().focus();
        }
        this.setState({description: nextProps.data});
    },
    getInitialState: function () {
        return({description: this.props.data});
    },
    handleKeyDown: function (e) {
        var key = e.keyCode;
        if (key == 13) { //enter is pressed, we need to add a new line underneath this one
            e.preventDefault();
            this.props.addDescription(this.props.descriptionNum);
        } else if (key == 8) { //backspace was pressed, check to see if line is empty and remove if so
            var value = this.refs.descInput.getDOMNode().value;
            if (value == null || value == '') {
                e.preventDefault();
                this.props.removeDescription(this.props.descriptionNum);
            }
        }
    },
    render: function () {
        return (
            <div className="description">
                <input type="text" onKeyDown={this.handleKeyDown} valueLink={this.linkState('description')} ref="descInput"/>
            </div>
        )
    }
});

module.exports = Description;

This component receives a string (or nothing), sets its state to contain that string, and uses the LinkedStateMixin to update the state whenever the value of the input changes, and vice-versa.

I thought I had no issues with these components, but the following Jest test...

descriptions-test.js

jest.dontMock('../js/descriptions.js');
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;

describe('Descriptions', function () {
    it('creates exactly two Description components when given a string array of length 2', function() {
        jest.dontMock('../js/description.js');
        var Description = require('../js/description.js');
        var info = ['foo','bar'];
        var Descriptions = require('../js/descriptions.js');
        var descriptions = TestUtils.renderIntoDocument(<Descriptions info={info}/>);
        var array = TestUtils.scryRenderedComponentsWithType(descriptions, Description);
        expect(array.length).toEqual(2);
    });
});

...fails with the following error:

● Descriptions › it mocks Description exactly twice when given info array of length 2
  - Error: Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. This usually means that you're trying to add a ref to a component that doesn't have an owner (that is, was not created inside of another component's `render` method). Try rendering this component inside of a new top-level component which will hold the ref.

...on this line:

var descriptions = TestUtils.renderIntoDocument(<Descriptions info={info}/>);

This makes no sense to me as the components render fine in the browser without any issues. It seems to only break when React's TestUtils attempts to do it.

Here are my dependencies:

package.json

"dependencies": {
  "jquery": "^2.1.4",
  "react": "^0.13.3",
  "react-tools": "^0.13.3"
},
"devDependencies": {
  "browserify": "^10.2.1",
  "gulp": "^3.8.11",
  "gulp-react": "^3.0.1",
  "gulp-shell": "^0.4.1",
  "gulp-streamify": "0.0.5",
  "gulp-uglify": "~1.1.0",
  "jest-cli": "^0.4.5",
  "node-libs-browser": "^0.5.2",
  "reactify": "^1.1.1",
  "vinyl-source-stream": "^1.1.0",
  "watchify": "^3.2.1",
  "webpack": "^1.9.10"
}

Does anyone know what might be causing this error?

like image 974
Michael Parker Avatar asked May 29 '15 18:05

Michael Parker


1 Answers

Move the require out of the test function.

jest.dontMock('../js/descriptions.js');
var React = require('react/addons');
var Description = require('../js/description.js');
var Descriptions = require('../js/descriptions.js');

describe('Descriptions', function () {
    it('creates exactly two Description components when given a string array of length 2', function() {
        var TestUtils = React.addons.TestUtils;
        var info = ['foo','bar'];
        var descriptions = TestUtils.renderIntoDocument(<Descriptions info={info}/>);
        var array = TestUtils.scryRenderedComponentsWithType(descriptions, Description);
        expect(array.length).toEqual(2);
    });
});
like image 58
Todd Avatar answered Oct 23 '22 02:10

Todd