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 :)
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:

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.
Most helpful comment
@ziyadmutawy i fixed it - use three backticks instead of one :-)
The issue is that
ApiRootFormis what has state, but what you're rendering is actuallyconnect(mapStateToProps, mapDispatchToProps)(ApiRootForm)- iow, an HOC aroundApiRootForm.Try adding
.dive({ context })to the end of yourshallowcall (although you'll need to refactor a bit so that the context is available; i'd strongly suggest you removeshallowWithStoreentirely and construct the options inline in each test).