How to test a TextField Material-UI element with React Jest? - reactjs

I built up a component with React and Material-UI. I'm using React and Redux.
my index.jsx looks like this:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import configureStore from '../store/configureStore';
import Routes from '../routes/routes';
import '../styles/main.less';
const store = configureStore();
render(
<Provider store={store}>
<MuiThemeProvider>
<Routes />
</MuiThemeProvider>
</Provider>,
document.getElementById('app'),
);
My component InputSearch looks like this:
import React, { PropTypes, Component } from 'react';
import TextField from 'material-ui/TextField';
class InputSearch extends Component {
...
render() {
return (
...
<TextField
defaultValue={this.props.keyword}
ref={(input) => { this.input = input; }}
autoFocus
hintText='Type a keyword'
errorText={this.state.errorText}
floatingLabelText='Search for keyword'
style={styles.textField}
/>
);
}
}
InputSearch.propTypes = {
keyword: PropTypes.string.isRequired,
resetSearch: PropTypes.func.isRequired,
searchBooks: PropTypes.func.isRequired,
toggleResultsOpacity: PropTypes.func.isRequired,
firstSearch: PropTypes.bool.isRequired,
};
export default InputSearch;
I'm using Airbnb Enzyme and Jest to build unit tests.
My test to the InputSearch component looks like this:
import React from 'react';
import { shallow, mount } from 'enzyme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import TextField from 'material-ui/TextField';
import InputSearch from '../components/InputSearch/InputSearch';
const resetSearchMock = jest.fn();
const searchBooksMock = jest.fn();
const toggleResultsOpacityMock = jest.fn();
const setup = () => {
const props = {
keyword: '',
resetSearch: resetSearchMock,
searchBooks: searchBooksMock,
toggleResultsOpacity: toggleResultsOpacityMock,
firstSearch: true,
};
const wrapper = shallow(<MuiThemeProvider><InputSearch {...props} /></MuiThemeProvider>);
return {
props,
wrapper,
};
};
describe('Initial test', () => {
test('Shows error message when input search is empty.', () => {
const { wrapper, props } = setup();
expect(wrapper.find(TextField).getValue()).toEqual('');
});
}
However, I'm getting the following error:
TypeError: wrapper.find(...).getValue is not a function
Can anyone help me reach the right way to check the value of the Material UI TextField, please?

I have been writing test for few days using mocha, enzyme and chai. The problem that comes with testing material-ui is these are inturn react component so they cannot be tested as you test regular html elements.
You can find out what property change by printing the whole component, like
console.log(wrapper.find('TextField').debug());
This will print the whole element for you, you can notice that the TestField has value prop which is what you are suppose to check because this prop is what decided the value in the TextField
So the code will go like this:
describe('Initial test', () => {
test('Shows error message when input search is empty.', () => {
const { wrapper, props } = setup();
expect(wrapper.find(TextField).props().value).to.equal('');
});
}
This is how I have been testing the TextField component.
Hope you find it helpful.

Please, if someone has a better solution answer my question. After some attempts, I figured out how to test some material UI components. Basically, we need to find the native html elements (input, button, etc) inside the material UI components via enzyme find. I also realized that "shallow", only do a one level deep search, as #Andreas Köberle said in comments below. To force a deep search in the DOM tree we need to use enzyme "mount". http://airbnb.io/enzyme/docs/api/ReactWrapper/mount.html
Here is my new test code.
import React from 'react';
import { shallow, mount } from 'enzyme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import { search } from '../sagas/search';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import Toggle from 'material-ui/Toggle';
import InputSearch from '../components/InputSearch/InputSearch';
const resetSearchMock = jest.fn();
const searchBooksMock = jest.fn();
const toggleResultsOpacityMock = jest.fn();
const muiTheme = getMuiTheme();
const props = {
keyword: '',
resetSearch: resetSearchMock,
searchBooks: searchBooksMock,
toggleResultsOpacity: toggleResultsOpacityMock,
firstSearch: true,
};
const setup = () => {
const wrapper = mount(
<InputSearch {...props} />,
{
context: {muiTheme},
childContextTypes: {muiTheme: React.PropTypes.object}
}
);
return {
props,
wrapper,
};
};
const { wrapper } = setup();
const textFieldMUI = wrapper.find(TextField);
const toggleAuthor = wrapper.find(Toggle).find('input#author');
const toggleTitle = wrapper.find(Toggle).find('input#title');
const button = wrapper.find(RaisedButton).find('button');
describe ('Initial test, validate fields', () => {
test('TextField component should exists.', () => {
expect(textFieldMUI).toBeDefined();
});
test('Shows an error message when input search is empty and the search button is clicked.', () => {
const { props } = setup();
props.keyword = '';
const wrapper = mount(
<InputSearch {...props} />,
{
context: {muiTheme},
childContextTypes: {muiTheme: React.PropTypes.object}
}
);
button.simulate('click');
expect(textFieldMUI.props().errorText).toEqual('This field is required');
});
test('Shows an error message when both "author" and "title" toggles are off and the search button is clicked.', () => {
toggleTitle.simulate('click');
button.simulate('click');
expect(textFieldMUI.props().errorText).toEqual('Select at least one filter (Title or Author)');
});
});

Enzyme shallow renders only one level deep, so in your case only MuiThemeProvider and InputSearch are rendered. You can use Jest snapshot feature to see what was rendered inside wrapper. You can use dive to force Enzyme to render the content of a component:
expect(wrapper.('InputSearch').dive().find(TextField).getValue()).toEqual('');
or you dont wrap the component with MuiThemeProvider and render InputSearch directly. You only need to add the styles prop. Now InputSearch is the top level component and Enzyme will render its content.
const setup = () => {
const props = {
keyword: '',
resetSearch: resetSearchMock,
searchBooks: searchBooksMock,
toggleResultsOpacity: toggleResultsOpacityMock,
firstSearch: true,
styles: {textfield: {fontSize:10}}
};
const wrapper = shallow(<InputSearch {...props} />);
return {
props,
wrapper,
};
};

const event = { 'target': { 'value': 'No' } };
wrapper.find('#selectBox').prop('onChange').call(null, event);

Related

Problems testing a Redux + React app with enzyme:

I have this component
import React, { useEffect } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { CircularProgress } from '#material-ui/core';
import { loadPhones } from './redux/actions/actions.js';
import TablePhones from './Table.js';
const mapStateToProps = (state) => state;
function mapDispatchToProps(dispatch) {
return {
loadPhones: () => {
dispatch(loadPhones());
},
};
}
export function App(props) {
useEffect(() => {
props.loadPhones();
}, []);
if (props.phones.data) {
return (
<div className="App">
<div className="introductoryNav">Phones</div>
<TablePhones phones={props.phones.data} />
</div>
);
}
return (
<div className="gridLoadingContainer">
<CircularProgress color="secondary" iconStyle="width: 1000, height:1000" />
<p className="loadingText1">Loading...</p>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
For whom ive written
import React from 'react';
import { render } from '#testing-library/react';
import { Provider } from "react-redux";
import App from './App';
import { shallow, mount } from "enzyme";
import configureMockStore from "redux-mock-store";
const mockStore = configureMockStore();
const store = mockStore({});
describe('App comp testing', () => {
it("should render without throwing an error", () => {
const app = mount(
<Provider store={store}>
<App />
</Provider>
).dive()
expect(app.find('.introductoryNav').text()).toContain("Phones");
});
})
But that test keeps failing
ypeError: Cannot read property 'data' of undefined
I also tried importing App as {App} instead and using shallow testing, but no luck. It gives the same erros, so im left without access to the context, and I cant keep doing my tests
How can I solve this?
You could use the non-default export of your component here and shallow render test if you pass your component the props and don't try to mock the store (if I recall correctly).
I was thinking something like this might work, tesing the "pure" non-store connected version of the component. This seems to be a popular answer for this question as this was asked (in a different way) before here:
import React from 'react';
import { App } from './App';
import { shallow } from "enzyme";
// useful function that is reusable for desturcturing the returned
// wrapper and object of props inside of beforeAll etc...
const setupFunc = overrideProps => {
const props = {
phones: {
...phones, // replace with a mock example of a render of props.phones
data: {
...phoneData // replace with a mock example of a render of props.phones.data
},
},
loadPhones: jest.fn()
};
const wrapper = shallow(<App {...props} />);
return {
wrapper,
props
};
};
// this is just the way I personally write my inital describe, I find it the easiest way
// to describe the component being rendered. (alot of the things below will be opinios on test improvements as well).
describe('<App />', () => {
describe('When the component has intially rendered' () => {
beforeAll(() => {
const { props } = setupFunc();
});
it ('should call loadPhones after the component has initially rendered, () => {
expect(props.loadPhones).toHaveBeenCalled();
});
});
describe('When it renders WITH props present', () => {
// we should use describes to section our tests as per how the code is written
// 1. when it renders with the props present in the component
// 2. when it renders without the props
beforeAll(() => {
const { wrapper, props } = setupFunc();
});
// "render without throwing an error" sounds quite broad or more like
// how you would "describe" how it rendered before testing something
// inside of the render. We want to have our "it" represent what we're
// actually testing; that introductoryNave has rendered with text.
it("should render an introductoryNav with text", () => {
// toContain is a bit broad, toBe would be more specific
expect(wrapper.find('.introductoryNav').text()).toBe("Phones");
});
it("should render a TablePhones component with data from props", () => {
// iirc toEqual should work here, you might need toStrictEqual though.
expect(wrapper.find('TablePhones').prop('phones')).toEqual(props.phones);
});
});
describe('When it renders WITHOUT props present', () => {
it("should render with some loading components", () => {
expect(wrapper.find('.gridLoadingContainer').exists()).toBeTruthy();
expect(wrapper.find('CircularProgress').exists()).toBeTruthy();
expect(wrapper.find('.loadingText1').exists()).toBeTruthy();
});
});
});

How to test hoc stateful component with React Hooks?

I have a Layout hoc component and I wanna guarantee the state changes.
Here my Layout component:
import React, { useState } from 'react'
import styles from './Layout.module.css'
import Toolbar from '../../components/UI/Navigation/Toolbar/Toolbar'
import SideDrawer from '../../components/UI/Navigation/SideDrawer/SideDrawer'
const Layout = (props) => {
const [sideDrawerIsVisible, setSideDrawerIsVisible] = useState(false)
const sideDrawerClosedHandler = () => {
setSideDrawerIsVisible(false)
}
const slideDrawerToggleHandler = () => {
setSideDrawerIsVisible((prevState) => !prevState)
}
return (
<>
<Toolbar
height="2em"
drawerToggleClicked={slideDrawerToggleHandler} />
<SideDrawer
open={sideDrawerIsVisible}
closed={sideDrawerClosedHandler} />
<main className={styles.Content}>
{props.children}
</main>
</>
);
}
export default Layout
The solution that I found is received the instance and use the useState().
I need the instance to set the state and verify it after the click action. The test in hand is should update state when drawer toggle clicked but according to the Enzyme documentation to "stateless" components we receive null to instance() but this changed in React Hooks. There is a better approach to test this situation?
import React from 'react';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Layout from './Layout';
import Toolbar from '../../components/UI/Navigation/Toolbar/Toolbar';
configure({ adapter: new Adapter() });
describe('<Layout />', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Layout>children...</Layout>);
});
it('should update state when drawer toggle clicked', () => {
const toolbar = wrapper.find(Toolbar);
wrapper.instance().setSideDrawerIsVisible(true);
toolbar.props().drawerToggleClicked();
expect(wrapper.sideDrawerIsVisible).toEqual(false);
});
});
Just don't try to init state through workaround ways. Interact with component through its public interface - I mean props.
it('displays drawer after toolbar is clicked', () => {
const toolbar = wrapper.find(Toolbar);
toolbar.props().drawerToggleClicked();
expect(wrapper.find(SideDrawer).props().open).toEqual(true);
});
it('hides drawer after toolbar is clicked for second time', () => {
const toolbar = wrapper.find(Toolbar);
toolbar.props().drawerToggleClicked();
toolbar.props().drawerToggleClicked();
expect(wrapper.find(SideDrawer).props().open).toEqual(false);
});
it('hides drawer initially shown after closing it from inside', () => {
const toolbar = wrapper.find(Toolbar);
toolbar.props().drawerToggleClicked(); // to display drawer
const sideDrawer = wrapper.find(SideDrawer);
sideDrawer.props().closed();
expect(sideDrawer.props().open).toEqual(false);
});
when you operate on instance() directly or use Enzyme's setState() you are bounding your tests to component's internals and tests become fragile with no advantages.

Why enzyme tests did not work in React.js?

I am using Enzyme tests within Create-React-App. In shallow rendering it works fine, but mount rendering throws error:
TypeError: Cannot read property 'favorites' of undefined
Test file looks like this:
import React, { Component } from "react";
import configureMockStore from "redux-mock-store";
import { shallow, mount } from "enzyme";
import { Provider } from "react-redux";
import Favorites from "../Landing/Favorites";
const mockStore = configureMockStore();
const store = mockStore({});
function setup() {
const props = {
favorites: 42
};
const wrapper = mount(
<Provider store={store}>
<Favorites {...props} />
</Provider>
);
return {
props,
wrapper
};
}
describe("Favorites component", () => {
const { wrapper } = setup();
it("should render list of favorites cards", () => {
expect(wrapper.prop("favorites")).toEqual(42);
});
});
Why did it happen?
.prop works different in mount and shallow. You can check the docs.
http://airbnb.io/enzyme/docs/api/ReactWrapper/prop.html
http://airbnb.io/enzyme/docs/api/ShallowWrapper/prop.html
When using mount, you can directly render Favorites component.
mount(<Favorites {...props} />)

Material UI + Enzyme testing component

I have component in React which I'm trying to test with Jest, unfortunately test do not pass.
The component code:
import React, {Component} from 'react';
import ProductItem from '../ProductItem/ProductItem';
import AppBar from "#material-ui/core/es/AppBar/AppBar";
import Tabs from "#material-ui/core/es/Tabs/Tabs";
import Tab from "#material-ui/core/es/Tab/Tab";
import {connect} from 'react-redux';
class ProductsTabsWidget extends Component {
state = {
value: 0
}
renderTabs = () => {
return this.props.tabs.map((item, index) => {
return item.products.length > 0 ? (<Tab key={index} label={item.title}/>) : false;
})
}
handleChange = (event, value) => {
this.setState({value});
};
renderConentActiveTab = () => {
if (this.props.tabs[this.state.value]) {
return this.props.tabs[this.state.value].products.map((productIndex) => {
return (<ProductItem key={productIndex} {...this.props.products[productIndex]} />);
});
}
}
render() {
let tabs = null;
let content = null;
if (this.props.tabs) {
tabs = this.renderTabs();
content = this.renderConentActiveTab();
}
return (
<div>
<AppBar position="static" color="default">
<Tabs
value={this.state.value}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary"
centered
scrollButtons="auto"
>
{tabs}
</Tabs>
</AppBar>
<div className="productWidget">
<div className="wrapper">
{content}
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
products: state.product.products,
}
}
export default connect(mapStateToProps)(ProductsTabsWidget);
I have tried to write proper test for this component, the code is below:
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import ProductsTabsWidget from "./ProductsTabsWidget";
configure({adapter: new Adapter()});
describe('ProductsTabsWidget - component', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<ProductsTabsWidget/>);
});
it('renders with minimum props without exploding', () => {
wrapper.setProps({
tabs: [],
products:[]
});
expect(wrapper).toHaveLength(1);
});
})
But when I'm running test I am getting error:
Test suite failed to run
F:\PRACA\reactiveShop\node_modules\#material-ui\core\es\AppBar\AppBar.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import _extends from "#babel/runtime/helpers/builtin/extends";
^^^^^^
SyntaxError: Unexpected token import
at new Script (vm.js:51:7)
at Object.<anonymous> (src/components/product/ProductsTabsWidget/ProductsTabsWidget.js:3:15)
I have tried testing with shallow, mount, render but it did not help. What am I missing?
My application is created on create-react-app.
It's something different when you're using #material-ui.
You've to use #material-ui's Built-in API(s). Such as createMount, createShallow, createRender in order to use enzyme's shallow, mount & render.
These APIs are built on top of enzyme, so you can't use enzyme directly for testing #material-ui.
Example of Shallow Rendering with #material-ui
import { createShallow } from '#material-ui/core/test-utils';
describe('<MyComponent />', () => {
let shallow;
before(() => {
shallow = createShallow();
});
it('should work', () => {
const wrapper = shallow(<MyComponent />);
});
});
Reference: Official Docs of #material-ui
Following is a humble attempt to provide a more complete answer from create-react-app and #material-ui perspective.
1. Create setupTests.js directly in src folder and paste the following code.
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });
2. The following is react stateless component which uses material-ui components.
import React from "react";
import TextField from "#material-ui/core/TextField";
const SearchField = props => (
<TextField InputProps={{ disableUnderline: true }} fullWidth
placeholder={props.placeholder}
onChange={props.onChange}
/>
);
export default SearchField;
Note that in the above component, the component expects parent component to pass the props for placeholder and onChange() event handler
3. Coming to the test case for for the above component we can write either in a way material-ui suggests or in a pure enzyme style. Both will work.
Pure Enzyme Style
import React from "react";
import { mount } from "enzyme";
import TextField from "#material-ui/core/TextField";
import SearchField from "../SearchField";
describe("SearchField Enzyme mount() ", () => {
const fieldProps = {
placeholder: "A placeholder",
onChange: jest.fn()
};
const Composition = props => {
return <SearchField {...fieldProps} />;
};
it("renders a <TextField/> component with expected props", () => {
const wrapper = mount(<Composition />);
expect(wrapper.childAt(0).props().placeholder).toEqual("A placeholder");
expect(wrapper.childAt(0).props().onChange).toBeDefined();
});
it("should trigger onChange on <SearchField/> on key press", () => {
const wrapper = mount(<Composition />);
wrapper.find("input").simulate("change");
expect(fieldProps.onChange).toHaveBeenCalled();
});
it("should render <TextField />", () => {
const wrapper = mount(<Composition />);
expect(wrapper.find(TextField)).toHaveLength(1);
expect(wrapper.find(TextField).props().InputProps.disableUnderline).toBe(
true
);
});
});
Material UI style
import React from "react";
import { createMount } from "#material-ui/core/test-utils";
import TextField from "#material-ui/core/TextField";
import SearchField from "../SearchField";
describe("SearchField", () => {
let mount;
const fieldProps = {
placeholder: "A placeholder",
onChange: jest.fn()
};
beforeEach(() => {
mount = createMount();
});
afterEach(() => {
mount.cleanUp();
});
it("renders a <TextField/> component with expected props", () => {
const wrapper = mount(<SearchField {...fieldProps} />);
expect(wrapper.props().placeholder).toEqual("A placeholder");
expect(wrapper.props().onChange).toBeDefined();
});
it("should trigger onChange on <SearchField/> on key press", () => {
const wrapper = mount(<SearchField {...fieldProps} />);
wrapper.find("input").simulate("change");
expect(fieldProps.onChange).toHaveBeenCalled();
});
});
5. The error you are getting is due to the fact that babel is not getting a chance to process your file. The create-react-app expects you to run tests like yarn run test and not like jest your/test/file.js. If you use latter babel won't be employed.
If you want to use jest to run the file you will have to write a jest.config.js file or configure jest in package.json file to use babel-jest + other babel dependencies to transpile your code before jest tries to execute tests.
I was in the same boat yesterday as I tried to use #material-ui for the first time and came here to get a more complete answer.
Something like this worked for me:
import {createMount} from '#material-ui/core/test-utils';
const WrappedComponent = () =>
<MUIThemeStuffEtc>
<MyComponent />
</MUIThemeStuffEtc>
const render = createMount();
const wrapper = render(<WrappedComponent />);
const state = wrapper.find(MyComponent).instance().wrappedInstance.state

Jest / React / Redux - MapDispatchToProps Undefined

I am trying to learn React w/ Jest / Enzyme.
I have a component that receives 2 props -
loadTenantListAction,
filterTenantListAction,
These props are passed in via mapDispatchToProps -
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import {
loadTenantListAction,
filterTenantListAction,
} from '../../store/actions';
import TenantList from './TenantList';
const mapStateToProps = tenants => ({ tenants });
const mapDispatchToProps = {
loadTenantListAction,
filterTenantListAction,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(TenantList)
);
I have declared propTypes in my component as such -
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class TenantList extends Component {
static propTypes = {
loadTenantListAction: PropTypes.func.isRequired,
filterTenantListAction: PropTypes.func.isRequired,
};
render() {
return <p>Foo</p>;
}
}
My unit test is failing now showing that these props are marked as required, but are undefined. I expect this, as I am not passing them into my test -
import React from 'react';
import { shallow } from 'enzyme';
import TenantListContainer from '../../../src/containers/TenantList';
import TenantList from '../../../src/containers/TenantList/TenantList';
describe('<TenantList />', () => {
it('should render the TenantList Component', () => {
const wrapper = shallow(<TenantListContainer />);
expect(wrapper.find(<TenantList />)).toBeTruthy();
});
});
I can pass the test doing something like
expect(
wrapper.find(
<TenantList
loadTenantListAction={() => {}}
filterTenantListAction={() => {}}
/>
)
).toBeTruthy();
But that does not seem right at all, nor do I expect to be able to write useful tests by carrying on like that.
How should I be handling props passed in via mapDispatchToProps?
You can pass props directly to your component in shallow method.
describe('<TenantList />', () => {
const props = {
loadTenantListAction: () => {}, // or you can use any spy if you want to check if it's called or not
filterTenantListAction () => {},
}
it('should render the TenantList Component', () => {
const wrapper = shallow(<TenantListContainer {...props} />);
expect(wrapper.find(<TenantList />)).toBeTruthy();
});
});

Resources