Enzyme not shallow rendering child - reactjs

I am trying to test that a child component exists by shallow-rendering it with Enzyme. However, it does not appear to be getting past the return of the component and instead shallow-renders <undefined />.
MainComponentContainer.js
import PropTypes from 'prop-types'
import React from 'react'
import createReactClass from 'create-react-class'
import ImmutablePropTypes from 'react-immutable-proptypes'
import MainComponent from './MainComponent'
import {addSelection} from '../../actions/betslip'
import {connect} from 'react-redux'
export const MainComponentContainter = createReactClass({
displayName: 'MainComponentCont',
propTypes: {
displayMode: PropTypes.string,
other: ImmutablePropTypes.map,
addSelection: PropTypes.func,
prices: ImmutablePropTypes.map,
selections: ImmutablePropTypes.map,
},
render() {
return (
<div>
{this.props.other.valueSeq().map(this.renderMain)}
</div>
)
},
renderMain(other) {
const yesOutcome = other.get('markets').first().get('outcomes').first()
const newPrice = this.props.prices.getIn([yesOutcome.get('id'), 'price'])
if (newPrice) {
return (
<MainComponent key={other.get('id')}
...some other props/>
)
}
return null
}
})
const mapStateToProps = () => {
return (state) => {
const displayMode = state.ui.get('displayMode')
const selections = state.stp.get('selections')
const prices = state.catalog.get('prices')
const other = state.catalog.get('other')
return {
displayMode,
other,
prices,
selections,
}
}
}
const mapDispatchToProps = {
addSelection,
}
export default connect(mapStateToProps, mapDispatchToProps)(MainComponentContainer)
MainComponent.js
This just basically returns another component to the above component.
return (
<div style={[styles.container, styles.container[displayMode]]}>
<div style={[styles.logo, styles.logo[displayMode]]}>
{renderLogo(displayMode, quoteExtraLogo)}
{displayMode === 'mobile' &&
renderButton({displayMode, selected, suspended, clickHandler})}
</div>
<div style={[styles.content, styles.content[displayMode]]}>
<h2 style={[styles.headline, styles.headline[displayMode]]}>
{title}
</h2>
<div style={[styles.offer, styles.offer[displayMode]]}>
<div style={[styles.details, styles.details[displayMode]]}>
<p style={[styles.market, styles.market[displayMode]]}>
{text}
</p>
<div>
<p style={[styles.improvedOdds, styles.improvedOdds[displayMode]]}>
<span style={styles.improvedOddsAt}>a</span> {newPrice}
</p>
<p style={[styles.previousOdds, styles.previousOdds[displayMode]]}>
invece di{' '}
<span className="strikethrough">
{oldPrice}
</span>
</p>
</div>
</div>
{displayMode === 'desktop' &&
renderButton({displayMode, selected, suspended, clickHandler})}
</div>
</div>
</div>
)
Test
describe.only('MainComponentContainer Component', () => {
beforeEach(() => {
sandbox = sinon.sandbox.create()
addSelectionSpy = sinon.spy()
})
afterEach(() => {
sandbox.restore()
})
function getOutput({
displayMode = 'mobile',
other = mockData,
addSelection = spy,
prices = pricesMock,
selections = selectionsMock,
} = {}) {
return shallow(
<MainComponentContainer
displayMode = {displayMode}
other = {mockData}
addSelection = {addSelection}
prices = {prices}
selections = {selections}
/>
)
}
it('should include a MainComponent component', () => {
const pb = getOutput().find('MainComponent')
expect(pb.length).to.equal(1)
})
When doing the above test (should include a MainComponent component), I get the following error:
AssertionError: expected 0 to equal 1
+ expected - actual
-0
+1
However I have logged out getOutput().debug(), and it returns <div><undefined /></div>.

The shallow renderer is intentionally limited to operating on only the root component so as to make the test more isolated. In the case of decorators or "wrapped" components like this, the wrapped component is not what we want to test. Since MainComponentContainer is a HOC, you face this problem.
There are two ways to get around this problem, either
First export the undecorated component
export default connect(mapStateToProps, mapDispatchToProps)(MainComponentContainer)
export {MainComponentContainer as ComponentContainer};
and test like
return shallow(
<ComponentContainer
displayMode = {displayMode}
other = {mockData}
addSelection = {addSelection}
prices = {prices}
selections = {selections}
/>
)
or use .dive
it('should include a MainComponent component', () => {
const pb = getOutput().dive().find('MainComponent')
expect(pb.length).to.equal(1)
})

Related

How to test if props are being rendered, in circumstances where props are being passed as an object

I'm using React Testing Library to test a cafe review app. I have a parent component CafeList that passes an object containing data about the cafes to a child component Cafe, which renders out the cafe data. The object being passed takes the form { name:name,photoURL:photoURL, id:cafe.id}, and I want to test that the name property is being rendered in Cafes.
I'm having trouble though because I don't know how to test a specific value of an object when using RTL - any suggestions?
Here's the parent component CafeList.jsx
import React, { useState,useEffect } from 'react'
import db from '../fbConfig'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes,setCafe] = useState([])
useEffect(() => {
let cafeArray = []
db.collection('cafes')
.get()
.then(snapshot => {
snapshot.forEach(cafe => {
cafeArray.push(cafe)
})
setCafe(cafeArray)
})
},[])
const [...cafeData] = cafes.map((cafe) => {
const { name, photoURL } = cafe.data()
return { name:name,photoURL:photoURL, id:cafe.id}
})
return(
<div className="cafe-container-container">
<h2 className = 'main-subheading'>Reviews</h2>
<Cafe cafes = {cafeData}/>
</div>
)
}
...and the child component Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = ({ cafes }) => {
return (
<div className="cafe-grid">
{
cafes.map((cafe) => {
return (
<Link
to={`/cafe-reviews/${cafe.id}`}
style={{ textDecoration: "none", color: "#686262" }}
>
<div className="cafe-container">
<h3>{cafe.name}</h3>
<img src={cafe.photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
and lastly, here's the test I wrote
import { render, screen } from '#testing-library/react'
import '#testing-library/jest-dom'
import Cafe from '../components/CafeList'
const testArray = [{name: 'this is the name',photoUrl:'photoURL',id: 'id'}]
test('is cafe name prop being passed ', () =>{
render(<Cafe cafes = {testArray}/>)
const nameElement = screen.getByText(/this is the name/i)
expect(nameElement).toBeInTheDocument()
})

How to test button prop in enzyme

Trying to test this component, and im getting this
error
TypeError: this.props.onItemAdded is not a function
I've referenced this but this solution doesn't really apply to my problem
Enzyme test: TypeError: expect(...).find is not a function
How would i test the button functionality being that the button is a prop ?
todo-add-item.test.js
import React from "react";
import { shallow } from "enzyme";
import TodoAddItem from './todo-add-item';
describe('Should render add item component', ()=> {
it('should render add item component', () => {
const wrapper = shallow(<TodoAddItem/>)
})
})
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
const wrapper =shallow(<TodoAddItem/>)
wrapper.find('button').simulate('click') // getting the type error here.
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
You don't pass any props to your component.
const wrapper =shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
You can check props with .props()
Eg:
console.log('props',wrapper.find('button').props());

Reactjs - Props Is Lost While Using It In A Child Component TypeError:

I have the following code in react passes props from stateless component to state-full one and I get TypeError while running.
However, when I use props with same name the error goes away!
Your help would be appreciated in advance
import React, { Component } from 'react';
class App extends Component {
state = {
title:'xxxxxxx',
show:true,
SampleData:[object, object]
}
render() {
const {SampleData} = this.state.SampleData
return (
<div>
<SampleStateless list = {SampleData}/>
</div>
);
}
}
export default App;
const SampleStateless = (props) => {
const {list} = props
return (
<div>
<SampleStatefullComponent secondlist = {list} />
</div>
);
}
class SampleStatefullComponent extends Component {
state = {
something:''
}
render () {
const {secondlist} = this.props
console.log(secondlist);
// I get data correctly in console
const items = secondlist.map (item => return {some js})
//TypeError: secondlist is undefined
return (
<div>
{items}
</div>
)
}
}
You are doing map on a string but map works only on arrays. Take a look at corrected code
Also should be
const {SampleData} = this.state;
but not
const {SampleData} = this.state.SampleData;
Updated code
import React, { Component } from 'react';
class App extends Component {
state = {
title:'xxxxxxx',
show:true,
SampleData:[{'id': "01", "name": "abc"}, {'id': "02", "name": "xyz"}]
}
render() {
const {SampleData} = this.state;
return (
<div>
<SampleStateless list = {SampleData}/>
</div>
);
}
}
export default App;
const SampleStateless = (props) => {
const {list} = props
return (
<div>
<SampleStatefullComponent secondlist = {list} />
</div>
);
}
class SampleStatefullComponent extends Component {
state = {
something:''
}
render () {
const {secondlist} = this.props
console.log(secondlist);
// I get data correctly in console
const items = {secondlist && secondlist.map (item => item.name)}
return (
<div>
{items}
</div>
)
}
}

Redux not Re-rendering React components even though store is updated

Hi I'm new to Redux and I'm using React and Redux to try to build a UI where I can drag and drop files (invoices in this case) into a portion of the UI, render them in a list and then be able to launch a popover to edit the metadata associated with each invoice. Dragging and dropping is all working fine - Redux is re-rendering the view each time a file is dropped and the list is being updated. However, when I try an click the edit button against each invoice the store is being updated but the props in my popover component are not. Indeed, it doesn't look like any re-rendering is happening at all when I attempt to click the edit invoice button
App.js
import React from 'react'
import AddInvoice from '../containers/AddInvoice'
import CurrentInvoiceList from '../containers/CurrentInvoiceList'
import ControlPopover from '../containers/ControlPopover'
const App = () => (
<div>
<AddInvoice />
<CurrentInvoiceList />
<ControlPopover />
</div>
)
export default App
containers/AddInvoice.js
import React from 'react'
import { connect } from 'react-redux'
import { addInvoice } from '../actions'
const recipientDataDefaults = {
name: '',
surname: '',
address: '',
phone: ''
};
const handleDragOver = event => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
const handleDragEnter = event => {
event.stopPropagation();
event.preventDefault();
};
const handleDragLeave = event => {
event.stopPropagation();
event.preventDefault();
};
let AddInvoice = ({ dispatch }) =>
const styles = {'minHeight': '200px', 'background': 'tomato'}
return (
<div style={styles}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={event => {
event.stopPropagation();
event.preventDefault();
const data = event.dataTransfer;
const files = data.files;
const newInvoiceUploads = Object.keys(files)
.map(key => files[key])
.map(file => {
const invoiceObject = {};
invoiceObject.files = [file];
invoiceObject.recipientData = Object.assign({}, recipientDataDefaults);
return invoiceObject;
});
newInvoiceUploads.forEach(invoice => dispatch(addInvoice(invoice)))
}}>
Drag an invoice here to upload
</div>
)
}
AddInvoice = connect()(AddInvoice)
export default AddInvoice
containers/ControlPopover.js
import { connect } from 'react-redux'
import { closePopoverWithoutSave } from '../actions'
import Popover from '../components/Popover/Popover'
const mapStateToProps = (state) => {
return {
isActive: !!state.isActive
}
}
const mapDispatchToProps = {
handleCancel: closePopoverWithoutSave
}
const ControlPopover = connect(
mapStateToProps,
mapDispatchToProps
)(Popover)
export default ControlPopover
containers/CurrentInvoiceList.js
import { connect } from 'react-redux'
import { showInvoiceEditPopover } from '../actions'
import InvoiceList from '../components/InvoiceList/InvoiceList'
const mapStateToProps = state => {
return {
invoices: state.invoices
}
}
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
const CurrentInvoiceList = connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceList)
export default CurrentInvoiceList
actions/index.js
let nextInvoiceId = 0
export const addInvoice = invoice => ({
type: 'ADD_INVOICE',
id: nextInvoiceId++,
invoiceData: invoice
})
export const showInvoiceEditPopover = invoice => ({
type: 'SHOW_POPOVER',
invoice
})
The popover reducer (combined in app but inlined here for brevity) reducers/index.js
const popover = (state = {}, action) => {
switch (action.type) {
case 'SHOW_POPOVER':
const popoverState = {}
popoverState.isActive = true
popoverState.data = action.invoice
return popoverState
case 'CLOSE_POPOVER_WITHOUT_SAVING':
const inactiveState = {}
inactiveState.isActive = false
inactiveState.data = {}
return inactiveState;
default:
return state
}
}
export default popover
components/InvoiceList.js
import React from 'react'
import PropTypes from 'prop-types'
import Invoice from '../Invoice/Invoice'
const InvoiceList = ({ invoices, handleEditInvoice }) => {
return (
<div>
{invoices.map(invoice =>
<Invoice
key={invoice.id}
invoice={invoice.invoiceData}
onClick={event => {
// here we invoke the action bound by the CurrentInvoiceList
// container
event.preventDefault()
handleEditInvoice(invoice)
}}
/>
)}
</div>
)
}
InvoiceList.propTypes = {
invoices: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
invoiceData: PropTypes.object
}).isRequired).isRequired,
handleEditInvoice: PropTypes.func.isRequired
}
export default InvoiceList
components/Invoice.js
import React from 'react'
import PropTypes from 'prop-types'
import TextInput from '../TextInput/TextInput';
import Button from '../Button/Button';
import './invoice.css'
const Invoice = ({ invoice, onClick }) => {
const fileNames = invoice.files.map((file, index) => {
return (<div key={index} className="invoice__file-title-legend">
{file.name}</div>);
});
return (
<div className="invoice">
<form className="invoice__form">
<TextInput id="invoice__input-amount" placeholder="enter invoice amount" label="Invoice Amount" />
<TextInput id="invoice__input-target" placeholder="enter payment target" label="Payment Target" />
<Button value="Add recipient" onClick={onClick} /> // clicking this button updates the store but does NOT re-render. Why?
</form>
<div className="invoice__files">{fileNames}</div>
</div>
)
}
Invoice.propTypes = {
onClick: PropTypes.func.isRequired,
invoice: PropTypes.object
}
export default Invoice
components/Popover/Popover.js
import React from 'react'
import PropTypes from 'prop-types'
import createModifiers from '../../lib/createModifiers';
import './Popover.css';
const Popover = ({ handleCancel, isActive }) => {
console.log('rendering popover component') // does not get called when invoice edit button is pressed
const popoverModifiers = createModifiers('popover', {
'is-active': isActive
})
return (
<div className={popoverModifiers}>
<div className="popover__header">
<button onClick={handleCancel}>x</button>
</div>
<div className="popover__content">
Popover content
</div>
</div>
)
}
Popover.propTypes = {
handleCancel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired
}
export default Popover
And finally the createModifiers Code for posterity. This code is merely producing some BEM modifier CSS classes based on a state boolean value passed in
const createModifiers = (element, modifiers) => {
const classList = Object.keys(modifiers)
.filter(key => modifiers[key])
.map(modifier => `${element}--${modifier}`)
.concat(element)
.join(' ')
return classList
}
export default createModifiers
I know this is a large amount of example code so I tried to keep it a brief and focused as possible whilst giving a comprehensive view of the application. Any help is most appreciated.
The problem is in containers/ControlPopover.js and the mapStateToProps function. The isActive property needs to be assigned to state.popover.isActive
I believe your problem is your mapDispatchToProp functions are not formatted properly.
You need to return an object that has methods. Those methods are what will be given to your connected component as props.
Example:
const mapDispatchToProps = ( dispatch ) => {
return {
doSomething: ( arguments ) => {
// here you can dispatch and use your arguments
}
};
}
doSomething is the prop that would be provided to the connected component.
All of your mapDispatchToProps functions are formatted improperly.
SIDE NOTE / OPINION - TLDR:
In the future if you have a lot of code to post, I believe it would be easier to digest if the pieces were linked together.
I.E.
// App.js
const App = () => (
<div>
<Header />
<Body />
<Footer />
</div>
);
The components appear in the order: header -> body -> footer. Provide the code for them in that order, with their actions, reducer, presentational, and container information in one block.
Header
// header.presentational.js ...
// header.container.js ... ( or where you mapStateToProps and connect )
// header.actions.js ...
// header.reducer.js ...
Body ...
Footer ...
I don't know if the code is different on your end, but your mapStateToDispatch function is still improperly formatted.
Change this...
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
To this:
const mapDispatchToProps = dispatch => ({
return {
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
};
})

Implementing React Redux

I am slowly learning React and also learning to implement it with Redux. But I seem to have hit a road block. So this is what I have so far.
/index.jsx
import './main.css'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App.jsx'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import ShoppingList from './reducers/reducer'
let store = createStore(ShoppingList)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
)
/actions/items.js
import uuid from 'node-uuid'
export const CREATE_ITEM = 'CREATE_ITEM'
export function createItem(item) {
return {
type: CREATE_ITEM,
item: {
id: uuid.v4(),
item,
checked: false
}
}
}
/reducers/reducer.js
import * as types from '../actions/items'
import uuid from 'node-uuid'
const initialState = []
const items = (state = initialState, action) => {
switch (action.type) {
case types.CREATE_ITEM:
return {
id: uuid.v4(),
...item
}
default:
return state;
}
}
export default items
/reducers/index.js
UPDATE:
import { combineReducers } from 'redux'
import items from './reducer'
const ShoppingList = combineReducers({
items
})
export default ShoppingList
/components/Item.jsx
import React from 'react';
import uuid from 'node-uuid'
export default class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
}
}
render() {
if(this.state.isEditing) {
return this.renderEdit();
}
return this.renderItem();
}
renderEdit = () => {
return (
<input type="text"
ref={(event) =>
(event ? event.selectionStart = this.props.text.length : null)
}
autoFocus={true}
defaultValue={this.props.text}
onBlur={this.finishEdit}
onKeyPress={this.checkEnter}
/>
)
};
renderDelete = () => {
return <button onClick={this.props.onDelete}>x</button>;
};
renderItem = () => {
const onDelete = this.props.onDelete;
return (
<div onClick={this.edit}>
<span>{this.props.text}</span>
{onDelete ? this.renderDelete() : null }
</div>
);
};
edit = () => {
this.setState({
isEditing: true
});
};
checkEnter = (e) => {
if(e.key === 'Enter') {
this.finishEdit(e);
}
};
finishEdit = (e) => {
const value = e.target.value;
if(this.props.onEdit) {
this.props.onEdit(value);
this.setState({
isEditing: false
});
}
};
}
/components/Items.jsx
import React from 'react';
import Item from './Item.jsx';
export default ({items, onEdit, onDelete}) => {
return (
<ul>{items.map(item =>
<li key={item.id}>
<Item
text={item.text}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</li>
)}</ul>
);
}
// UPDATE: http://redux.js.org/docs/basics/UsageWithReact.html
// Is this necessary?
const mapStateToProps = (state) => {
return {
state
}
}
Items = connect(
mapStateToPros
)(Items) // `SyntaxError app/components/Items.jsx: "Items" is read-only`
//////////////////////////////////////
// Also tried it this way.
//////////////////////////////////////
Items = connect()(Items)
export default Items // same error as above.
Tried this as well
export default connect(
state => ({
items: store.items
})
)(Items) // `Uncaught TypeError: Cannot read property 'items' of undefined`
UPDATE:
After many attempts #hedgerh in Gitter pointed out that it should be state.items instead. so the solution was
export default connect(
state => ({
items: state.items
})
)(Items)
credits to #azium as well.
/components/App.jsx
export default class App extends React.Component {
render() {
return (
<div>
<button onClick={this.addItem}>+</button>
<Items />
</div>
);
}
}
What am I missing here in order to implement it correctly? Right now it breaks saying that Uncaught TypeError: Cannot read property 'map' of undefined in Items.jsx. I guess it makes sense since it doesn't seem to be hooked up correctly. This is the first part of the app, where the second will allow an user to create a many lists, and these lists having many items. I will probably have to extract the methods from Item.jsx since the List.jsx will do pretty much the same thing. Thanks
You're missing connect. That's how stuff gets from your store to your components. Read the containers section from the docs http://redux.js.org/docs/basics/UsageWithReact.html
import React from 'react'
import Item from './Item.jsx'
import { connect } from 'react-redux'
let Items = ({items, onEdit, onDelete}) => {
return (
<ul>{items.map(item =>
<li key={item.id}>
<Item
text={item.text}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</li>
})
</ul>
)
}
export default connect(
state => ({
items: state.items
})
)(Items)
Also you seem to be expecting onEdit and onDelete functions passed from a parent but you're not doing that so those functions will be undefined.

Resources