Enzyme: Why is shallow rendered component have a default state to {}

Created on 14 Nov 2017  路  10Comments  路  Source: enzymejs/enzyme

I had a crash in one of my component that was not seen by a test in shallow mode.

My crash is due to my component using a state value without instanciating it first in the constructor.
When I mount the component with enzyme I reproduce the crash (trying to access key of null).
But in Shallow mode I saw that the default state of the component is an empty object (so no crash at all).

Is there a reason for this behaviour ?

Thank you :)

question

Most helpful comment

@ziyadmutawy i fixed it - use three backticks instead of one :-)

The issue is that ApiRootForm is what has state, but what you're rendering is actually connect(mapStateToProps, mapDispatchToProps)(ApiRootForm) - iow, an HOC around ApiRootForm.

Try adding .dive({ context }) to the end of your shallow call (although you'll need to refactor a bit so that the context is available; i'd strongly suggest you remove shallowWithStore entirely and construct the options inline in each test).

All 10 comments

Can you provide example component and test code that you'd expect to crash with shallow (and does crash with mount), but doesn't?

any update on this guys? im trying to render a form component that has default states that get modified by an onclick event from a button on the form but I cannot write a test against it if default states specified in the constructor of my component are not available during a shallow or mount test

@ziyadmutawy same question :-) can you provide example code, so we can use it to create a test case?

Regardless, it sounds like enzyme is helping expose a bug in your implementation code - namely that you forgot to initialize your state.

sure thing! thanks for getting back to me!

My Component:

class ApiRootForm extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            rootApiArray: [...props.taxiiResources.rootApiArray],
            selectedRootApi: [],
            showEditDialogue: false,
            showAddDialogue: false,
            showSaveDialogue:false
        };
    }


    componentWillReceiveProps(nextProps){
        console.log("Got new props: ", nextProps);
        this.setState({rootApiArray: [...nextProps.taxiiResources.rootApiArray], selectedRootApi: []})
    }

    isSelected = (index) => {
        return this.state.selectedRootApi.indexOf(index) !== -1;
    };

    handleRowSelection = (selectedRow) => {
        this.setState({
            selectedRootApi: selectedRow
        });
    };

    _addApiRoot(apiRootObject){
        let newRootApiArray = this.state.rootApiArray;
        newRootApiArray.push(apiRootObject);
        this.setState({rootApiArray:newRootApiArray, showAddDialogue:false})
    }

    _deleteApiRoot(){
        let newRootApiArray = this.state.rootApiArray;
        newRootApiArray.splice(this.state.selectedRootApi[0], 1);
        this.setState({rootApiArray:newRootApiArray, selectedRootApi:[]})
    }

    _updateApiRoot(apiRootObject){
        let newRootApiArray = this.state.rootApiArray;
        newRootApiArray[this.state.selectedRootApi[0]] = apiRootObject;
        this.setState({showEditDialogue: false, rootApiArray:newRootApiArray, selectedRootApi:[]})
    }


    _renderTableBody(){
        // Need to create an array of Table rows
        let tableBody = [];

        _.forEach(this.state.rootApiArray, (value, index) => {
            let rootApiObjectKey = _.keys(value)[0];
            let rootApiObject = _.get(value, rootApiObjectKey);
            tableBody.push(
                <TableRow key={index} selected={this.isSelected(index)}>
                    <TableRowColumn>{rootApiObjectKey}</TableRowColumn>
                    <TableRowColumn>{rootApiObject.title}</TableRowColumn>
                    <TableRowColumn>{rootApiObject.description}</TableRowColumn>
                    <TableRowColumn>{rootApiObject.versions}</TableRowColumn>
                    <TableRowColumn>{rootApiObject.max_content_length}</TableRowColumn>
                </TableRow>
            );
        });
        return tableBody;
    }

    _getCurrentSelected(){
        return this.state.rootApiArray[this.state.selectedRootApi[0]]
    }


    render() {
        //console.log("API ROOT FORM ",this.state);
        //console.log(this.props);
        return (
            <div style={{textAlign:'center'}}>
                <CardTitle title={"API Roots"}/>
                <div style={{margin: '15px'}}>
                <Table onRowSelection={(index)=>this.handleRowSelection(index)}>
                    <TableHeader>
                        <TableRow>
                            <TableHeaderColumn>API Root</TableHeaderColumn>
                            <TableHeaderColumn>Title</TableHeaderColumn>
                            <TableHeaderColumn>Description</TableHeaderColumn>
                            <TableHeaderColumn>Versions</TableHeaderColumn>
                            <TableHeaderColumn>Max Content Length</TableHeaderColumn>
                        </TableRow>
                    </TableHeader>
                    <TableBody deselectOnClickaway={false}>
                        {this._renderTableBody()}
                    </TableBody>
                </Table>
                </div>

                <div style={{margin: '10px'}}>
                    <span>
                         <FlatButton
                             id={"AddApiRoot"}
                             label={"Add"}
                             onClick={()=>{this.setState({showAddDialogue:true})}}
                         />
                    <FlatButton
                        id={"DeleteApiRoot"}
                        label={"Delete"}
                        disabled={this.state.selectedRootApi.length===0}
                        onClick={()=>{this._deleteApiRoot()}}
                    />
                    <FlatButton
                        id={"EditApiRoot"}
                        label={"Edit"}
                        disabled={this.state.selectedRootApi.length===0}
                        onClick={()=>{this.setState({showEditDialogue:true})}}
                    />
                    </span>

                    <span style={{marginLeft:'40px'}}>
                        <FlatButton
                            id={"SaveApiRootsForm"}
                            primary={true}
                            label={"Save"}
                            disabled={_.isEqual(this.state.rootApiArray,this.props.taxiiResources.rootApiArray)}
                            onClick={()=>this.props.updateApiRootArray(this.state.rootApiArray, AppConfig.apiRootServiceEndpoint)}
                        />
                    <FlatButton
                        id={"ResetApiRootsForm"}
                        label={"Reset"}
                        secondary={true}
                        disabled={_.isEqual(this.state.rootApiArray,this.props.taxiiResources.rootApiArray)}
                        onClick={()=>{this.setState({rootApiArray:[...this.props.taxiiResources.rootApiArray]})}}
                    />
                    </span>


                </div>


                <AddApiRootDialogue
                    open={this.state.showAddDialogue}
                    onCancelCallback={()=>{this.setState({showAddDialogue:false})}}
                    onAddCallback={(apiRootEndpoint, apiRootObject)=>{this._addApiRoot(apiRootEndpoint, apiRootObject)}}
                />

                {this.state.selectedRootApi.length===0 ? null :
                    <EditApiRootDialogue
                    open={this.state.showEditDialogue}
                    apiRootObject={this._getCurrentSelected()}
                    onCancelCallback={()=>{this.setState({showEditDialogue:false})}}
                    onEditCallback={(updatedApiRootObject)=>this._updateApiRoot(updatedApiRootObject)}/>
                }

            </div>
        );
    }

}


function mapStateToProps({taxiiResources}) {
    return {taxiiResources}
}

function mapDispatchToProps(dispatch) {
    return {
        updateApiRootArray: function (newApiRootArray, apiEndpoint) {
            dispatch(updateApiRootArray(newApiRootArray, apiEndpoint))
        }
    }
}


ApiRootForm.propTypes = {
    taxiiResources: PropTypes.object.isRequired,
    updateApiRootArray: PropTypes.func.isRequired,
};


export default connect(mapStateToProps, mapDispatchToProps)(ApiRootForm)

My test case:

describe('Testing Page: ApiRootForm.test.js', () => {

    test("should  shallow render and not render the form since the default state of the is null", () => {
        const store = createMockStore(loggedInWithData);
        let shallowWrapper = shallowWithStore(<ApiRootForm/>, store);
        console.log(shallowWrapper.state()) //result is {}
    });


});

My helper methods used in test cases:

import { shallow, mount } from 'enzyme';
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import PropTypes from 'prop-types';

export const shallowWithStore = (component, store) => {
    const context = {
        store,
    };
    return shallow(component, { context });
};


export const mountWithStore = (component, store) => {
    let muiTheme = getMuiTheme(darkBaseTheme);
    const context = {
        store,
        muiTheme
    };

    const childContextTypes = {muiTheme: PropTypes.object, store: PropTypes.object}

    return mount(component, {context, childContextTypes});
};

Thanks again for all the help!

uhh, first time using the code block insert, but it seems to be skipping some lines. so forgive the crappy formatting 馃槄

@ziyadmutawy i fixed it - use three backticks instead of one :-)

The issue is that ApiRootForm is what has state, but what you're rendering is actually connect(mapStateToProps, mapDispatchToProps)(ApiRootForm) - iow, an HOC around ApiRootForm.

Try adding .dive({ context }) to the end of your shallow call (although you'll need to refactor a bit so that the context is available; i'd strongly suggest you remove shallowWithStore entirely and construct the options inline in each test).

@ljharb thanks for the pro tips (especially with the block code 馃槃 ). I did whatcha mentioned and it looks good so far. Lemme know whatcha think of this? I want to ensure that w.e I do is the proper way and not duct taped.

    test("should  shallow render with state", () => {
        const store = createMockStore(loggedInWithData);
        let muiTheme = getMuiTheme(darkBaseTheme);
        const context = {
            store,
            muiTheme
        };
        let shallowWrapper = shallow(<ApiRootForm/>, {context}).dive({context});
        console.log(shallowWrapper.state())
    });

Result in terminal:

image

Yes, that鈥檚 exactly correct!

@clementdubois hopefully this discussion has resolved your issue; if not, happy to reopen

@ljharb just want to thank you, was just pouring over a few of your threads relating to this topic and this one finally fixed it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

timhonders picture timhonders  路  3Comments

amcmillan01 picture amcmillan01  路  3Comments

thurt picture thurt  路  3Comments

dschinkel picture dschinkel  路  3Comments

aweary picture aweary  路  3Comments