Jest testing of hook onClick calls - reactjs

This error appears when running this onClick call test with Jest, does anyone know why?
I'm developing an app with React, and I'm new to its testing framework. I'm trying to develop a testing method to check if an onClick button is clicked. I've read that I need a "spy" function to get the information about whether or not it's been called.
The preference is to keep an enzyme and jest test stack.
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
46 | test("onClick calls props.onClick", () => {
47 | nodes.simulate("click");
> 48 | expect(onClickMock.mock.calls.length).toBe(1);
Since I'm mocking onClick in a way that was to return the value 1
Test
import React from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "#wojtekmaj/enzyme-adapter-react-17";
import { expect } from "#jest/globals";
import { findByTestId } from "../../utils/test";
import Accordion from "./accordion.jsx";
Enzyme.configure({ adapter: new Adapter() });
const defaultProps = {
opened: {},
Accordion: {},
};
const setup = (props = {}) =>
shallow(<Accordion {...props} {...defaultProps} />).dive();
describe("Function calls", () => {
let wrapper, nodes, props;
describe("Button", () => {
let onClickMock;
beforeEach(() => {
onClickMock = jest.fn();
props = {onClick: onClickMock};
wrapper = setup(props);
nodes = findByTestId(wrapper, "arrow");
});
test("onClick calls props.onClick", () => {
nodes.simulate("click");
expect(onClickMock.mock.calls.length).toBe(1);
});
});
});
Component
import { useState } from 'react';
import styled from 'styled-components';
import { IoIosArrowForward, IoIosArrowDown } from 'react-icons/io';
import styles from '../../variables';
const Accordion = (props) => {
const [opened, setOpened] = useState(false);
const renderArrow = () => {
if (!opened) return <IoIosArrowForward className="is-size-4" color={styles.primaryBlue} />
return <IoIosArrowDown className="is-size-4" color={styles.primaryBlue} />
}
const renderPanel = () => {
if (opened) return <Panel>{props.children}</Panel>
}
const togglePanel = () => {
setOpened(!opened)
}
return (
<AccordionTile data-testid="accordionTile" className={props.className + " px-6 py-5 is-clickable"}>
<p data-testid="arrow" onClick={togglePanel} className="is-flex is-flex-direction-row is-align-items-flex-start is-justify-content-flex-start is-size-5">
{renderArrow()} {props.title}
</p>
{renderPanel()}
</AccordionTile>
);
}
export default Accordion
Could someone guide me to finish testing this onClick

So you click and check that content appears:
wrapper = setup({
children: <span id="test" />
});
// accordion is collapsed yet
expect(wrapper.find('span#test')).toHaveLength(0);
nodes.simulate('click');
// accordion is expanded; children nodes are rendered
expect(wrapper.find('span#test')).toHaveLength(1);
Also you better switch from shallow() to mount() before you run into issues with useEffect is not called. You will still be able to avoid rendering some nested components by explicitly mocking them:
jest.mock('react-icons/io', () => ({
IoIosArrowForward: (props) => <span {...props} />,
IoIosArrowDown: props => <span {...props} />
}));
And also in way you do it now:
<Accordion {...props} {...defaultProps} />
your defaultProps overwrite props with the same name. To have it working in reasonable way(when defaultProps can be overwritten by props if you want to) it should go in reverse order:
<Accordion {...defaultProps} {...props} />

Related

Simple React Test Failing Even Though Correct Props Being Passed In

I'm trying to run what I assume should be a fairly simple test on a modal component within my React project, but for whatever reason the test keeps failing. And that's the case even though I can see that I am passing in the correct props that should make the component render as expected.
My modal code looks lke this:
import React from 'react';
import { Dialog, DialogFooter, DefaultButton, PrimaryButton, DialogType } from '#fluentui/react';
const Modal = (props) => {
console.log('Modal is firing...', props); // I see the correct props here, passed in via the test
if (!props.showModal) return null;
if (props.showModal) {
return (
<Dialog
hidden={!props.showModal}
data-testid="modal"
dialogContentProps={{
type: DialogType.normal,
title: props.title,
subText: props.subText,
}}
modalProps={{
isBlocking: false,
}}
>
<DialogFooter>
<PrimaryButton
text="Yes"
onClick={() => {
props.confirm();
props.close();
}}
/>
<DefaultButton
text="No"
onClick={() => {
props.close();
}}
/>
</DialogFooter>
</Dialog>
);
};
}
export default Modal;
And here is my test:
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import Modal from './Modal';
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});
it('renders modal correctly', () => {
const props = { title: "A Test Title", subText: "Test subtext", showModal: true };
const { getByText } = render(<Modal props={props} />);
expect(getByText('Test subtext')).toBeInTheDocument();
});
And the error I'm getting upon test failure is this:
Unable to find an element with the text: Test subtext. This could be because the text is broken up by multiple elements. In this case,
you can provide a function for your text matcher to make your matcher
more flexible.
In your test, try spreading the props, like:
render(<Modal {...props} />)

How do you use Enzyme to check for changes to a React Hooks component after onClick?

I am trying to write a simple integration test in my 100% React Hooks (React v16.12) project with Enzyme (v3.10), Jest (v24.0) and TypeScript where if I click a button component in my App container, another component displaying a counter will go up by one. The current value of the counter is stored in the state of the App container (see snippets below).
Basically, I mount the App component to render its children, then try to simulate a click on the button with Enzyme and check the props of the counter display component to see if its value has gone up. But nothing happens. Not only does the onClick handler not get called but I don't seem to be able to retrieve the value prop I pass to the PaperResource component. So basically I can't test the counter display changes when I click on the button in my Enzyme integration test! The test asserts that the value prop goes from 0 to 1, but this assertion fails without an error per seenter code here. Is this because Enzyme support for Hooks is still not there yet or am I doing something daft here? When I run the app on my browser, everything works as expected.
Here's my integration test
import React from 'react';
import App from './App';
import { mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
describe('App', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
act(() => {
wrapper = mount(<App />);
});
});
describe('when make paper button is clicked', () => {
beforeEach(() => {
act(() => {
wrapper.find('.make-paper__button').simulate('click');
});
});
it('should increase paper resource', () => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
});
});
});
And here is my React code
import React, { useState } from 'react';
import './App.scss';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
const App: React.FC = () => {
const [ resources, setResources ] = useState({
paper: 0,
});
const handleMakePaperButtonClick = () => {
setResources({
...resources,
paper: resources.paper + 1,
});
};
return (
<div className="App">
<MakePaperButton onClick={handleMakePaperButtonClick} />
<div className="resources">
<PaperResource value={resources.paper} />
</div>
</div>
);
}
export default App;
My components are very simple
// PaperResource.tsx
import React from 'react';
export default (props: { value: number }) => (
<div className="resources__paper">
<span>Paper: {props.value}</span>
</div>
);
// MakePaperButton.tsx
import React from 'react';
export default (props: { onClick: () => void }) => (
<div className="make-paper__button">
<button onClick={props.onClick}>Make Paper</button>
</div>
);
The only solution I've found so far is wrapping the expect statement in a setTimeout().
it('should increase paper resource', () => {
setTimeout(() => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
}, 0);
});

How to test styled Material-UI components wrapped in withStyles using react-testing-library?

I am trying to create a test with a styled Material-UI component using react-testing-library in typescript. I'm finding it difficult to access the internal functions of the component to mock and assert.
Form.tsx
export const styles = ({ palette, spacing }: Theme) => createStyles({
root: {
flexGrow: 1,
},
paper: {
padding: spacing.unit * 2,
margin: spacing.unit * 2,
textAlign: 'center',
color: palette.text.secondary,
},
button: {
margin: spacing.unit * 2,
}
});
interface Props extends WithStyles<typeof styles> { };
export class ExampleForm extends Component<Props, State> {
async handleSubmit(event: React.FormEvent<HTMLFormElement>) {
// Handle form Submit
...
if (errors) {
window.alert('Some Error occurred');
return;
}
}
// render the form
}
export default withStyles(styles)(ExampleForm);
Test.tsx
import FormWithStyles from './Form';
it('alerts on submit click', async () => {
jest.spyOn(window,'alert').mockImplementation(()=>{});
const spy = jest.spyOn(ActivityCreateStyles,'handleSubmit');
const { getByText, getByTestId } = render(<FormWithStyles />)
fireEvent.click(getByText('Submit'));
expect(spy).toHaveBeenCalledTimes(1);
expect(window.alert).toHaveBeenCalledTimes(1);
})
jest.spyOn throws the following error Argument of type '"handleSubmit"' is not assignable to parameter of type 'never'.ts(2345) probably because ExampleForm in wrapped in withStyles.
I also tried directly importing the ExampleForm component and manually assigning the styles, was couldn't do so:
import {ExampleForm, styles} from './Form';
it('alerts on submit click', async () => {
...
const { getByText, getByTestId } = render(<ActivityCreateForm classes={styles({palette,spacing})} />)
...
}
Got the following error: Type '{ palette: any; spacing: any; }' is missing the following properties from type 'Theme': shape, breakpoints, direction, mixins, and 4 more.ts(2345)
I'm finding it difficult to write basic tests in Typescript for Material-UI components with react-testing-library & Jest due to strong typings and wrapped components. Please Guide.
First of all when you use render method of react-testing-library you don't need to worry about using withStyles or any wrapper because at the end it renders the component as it could be in the real dom so you can write your tests normally.
Then as far as I can see you are doing the same thing I did when I was starting with tests (it means you are going to become good at it ;). You are trying to mock an internal method and that is not the best approach to follow because what you need to do is to test the real method.
So let's image that we have a Register users component.
src/Register.tsx
import ... more cool things
import * as api from './api';
const Register = () => {
const [name, setName] = useState('');
const handleNameChange = (event) => {
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
if (name) {
api.registerUser({ name });
}
};
return (
<form onSubmit={handleSubmit}>
<TextField
id='name'
name='name'
label='Name'
fullWidth
value={name}
onChange={handleNameChange}
/>
<Button data-testid='button' fullWidth type='submit' variant='contained'>
Save
</Button>
</form>
);
}
The component is pretty simple, its a form with an input and a button. We are using react hooks to change the input value and based on that we call or not api.registerUser when handleSubmit event is fired.
To test the component the first thing we need to do is to mock api.registerUser method.
src/__tests__/Register.tsx
import * as api from '../api'
jest.mock('../api')
api.registerUser = jest.fn()
This will allow us to see if that method is called or not.
The next thing to do is ... write the tests, in this scenario we can test two things to see if handleSubmit is working correctly.
Not to call api.registerUser if name is empty.
it('should not call api registerUser method', () => {
const { getByTestId } = render(<Register />)
fireEvent.click(getByTestId('button'))
expect(api.registerUser).toHaveBeenCalledTimes(0)
})
Call api.registerUser if name is not empty.
it('should call api registerUser method', () => {
const { getByLabelText, getByTestId } = render(<Register />)
fireEvent.change(getByLabelText('Name'), { target: { value: 'Steve Jobs' }})
fireEvent.click(getByTestId('button'))
expect(api.registerUser).toHaveBeenCalledTimes(1)
})
In this last test implicitly we are testing handleNameChange too because we are changing the name :) so name is not going to be empty and registerUser is going to be called.
The example with withStyles and typescript is in this repo.
The demo is here.
Why don't you use enzyme with Full DOM Rendering?
You can use simulate method to simulate events on mounted components.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
const { count } = this.state;
return (
<div>
<div className={`clicks-${count}`}>
{count} clicks
</div>
<a href="url" onClick={() => { this.setState({ count: count + 1 }); }}>
Increment
</a>
</div>
);
}
}
const wrapper = mount(<Foo />);
expect(wrapper.find('.clicks-0').length).to.equal(1);
wrapper.find('a').simulate('click');
expect(wrapper.find('.clicks-1').length).to.equal(1);
You can use unwrap to unwrap the wrapped styled component then test it
import { unwrap } from '#material-ui/core/test-utils';
import {ExampleForm, styles} from './Form';
it('alerts on submit click', async () => {
...
const unwrapped = unwrap(ExampleForm);
...
}
Then you can do he required testing on the unwrapped object

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

How to improve test of react component with jest?

I need help for understand how to improve my test ?
I covered branches tests to 100% but can't do statements tests, functions tests and lines tests so how to covered these ?
Here is my code-coverage (provided by jest):
And here is my test:
/**
* Testing our ItemList component
*/
import { shallow } from 'enzyme';
import React from 'react';
import { BootstrapProvider } from 'bootstrap-styled';
import ItemList from '../index';
const renderComponent = (props = {}) => shallow(
<BootstrapProvider>
<ItemList {...props} />
</BootstrapProvider>
);
describe('<ItemList />', () => {
it('should render an <ItemList /> tag', () => {
const renderedComponent = renderComponent();
expect(renderedComponent.find('ItemList').length).toBe(1);
});
});
Any advice are welcome.
Firstly, you can see that the this.props is highlighted and that you have set within the component the props className with a className by default.
You could test the presence of the default className and also trying to set one:
it('should render an <ItemList /> with a className', () => {
const renderedComponent = renderComponent({
className:'className-test'
});
expect(renderedComponent.find('ItemList').hasClass('className-test')).toEqual(true);
});
it('should render an <ItemList /> with a className 'item' by default ', () => {
const renderedComponent = renderComponent();
expect(renderedComponent.find('ItemList').hasClass('item')).toEqual(true);
});
As for the rendering, I suggest you follow the advice given in the comment.

Resources