Testing a CallBack function in a function which get triggered forButton Onclick using jest - reactjs

I have a React child component which has button
export function Banner({argumentSetter}){
function handleOnClick(){
argumentSetter(argument.READ);
}
return(
<div>
<Button onClick={handleOnClick}>
<Icon name="delete" type="filled">
Discard
</Icon>
</Button>
</div>
)
}
And I have my argumentSetter in my parent component defined as following,
const [argument,setArgument] = useState<Argument>(argument.EDIT);
argumentSetter = useCallBack((val)=>{
setArgument(val);
},[argument]);
return(
<div>
<Banner argumentSetter={argumentSetter}/>
</div>
)
How to get 100% test coverage using jest.

To test the banner, your code should be like the following
import React from "react";
import { mount } from "enzyme";
import { Banner } from "./Banner.js";
import { argument } from "./arguments.js";
it("Button click leads to argument.READ", async () => {
let promiseResolve = null;
const argPromise = new Promise((resolve) => {
promiseResolve = resolve;
});
const argumentSetter = (arg) => promiseResolve(arg);
const banner = mount(<Banner argumentSetter={argumentSetter} />);
banner.find("button").simulate("click");
const newArg = await argPromise;
expect(newArg).toEqual(argument.READ);
});
Explanation:
We create an externally fulfillable promise variable, called argPromise which will resolve when promiseResolve is called, which is called when the argumentSetter is called. Hence, when the button click is simulated, it will resolve the updated argument to newArg variable (which should be argument.READ), and hence you can test if it matches your expectation.
This should hence cover all lines of your Banner component during testing.

Related

Mock function doesn't get called when inside 'if' statement - React app testing with jest and enzyme?

I am writing a test case for my react app and I'm trying to simulate a button click with a mock function. I'm passing the mock function as a prop and I'm calling the function inside an 'if' statement but the mock function doesn't get called and the test fails but if i call the function without the 'if' statement it gets called and the test passes. Why is this happening?
Form.js
const Form = ({ text, incompleteList, setIncompleteList }) => {
const submitTodoHandler = (e) => {
e.preventDefault()
if (text !== '') {
setIncompleteList([...incompleteList, { name: text, id: Math.random() * 1000 }])
}
}
return (
<form action='' autoComplete='off'>
<button type='submit' className='todo-button' onClick={submitTodoHandler}>
add
</button>
</form>
)
}
export default Form
Form.test.js
import Enzyme, { shallow, mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import Form from '../components/Form'
Enzyme.configure({ adapter: new Adapter() })
test('Form calls setIncompleteList prop on add button onClick event', () => {
const mockfn = jest.fn()
const wrapper = mount(<Form setIncompleteList={mockfn} />)
wrapper.find('button').simulate('click')
expect(mockfn).toHaveBeenCalled()
})
I'm using react 16.
The problem was I did not pass the 'text' props to the form component and the comparison failed to take place that's why the mock doesn't get called and the test failed.
<Form text='mock' setIncompleteList={mockfn} />
Pass value and incompleteList while mounting the component
test('Form calls setIncompleteList prop on add button onClick event', () => {
const mockfn = jest.fn()
const wrapper = mount(<Form text='mock'
incompleteList={[{name: 'sarun', id: 1001}]} setIncompleteList={mockfn} />)
wrapper.find('button').simulate('click')
expect(mockfn).toHaveBeenCalled()
})
you can also set a default value for incompletelist like below so that no need to pass incompletelist while mounting the component,
const Form = ({ text, incompleteList = [], setIncompleteList }) => {
}

How can I test a click event which changes a useState state with enzyme?

I have the following component:
import React, { useState } from "react";
import { Button, ThirteenBold } from "#selfdecode/sd-component-library";
import { PlayIcon } from "assets/icons";
import { TourButtonProps } from "./interfaces";
import { WelcomeVideoModal } from "../../modals/welcome-video-modal";
/**
* The tour button.
*/
export const TourButton: React.FC<TourButtonProps> = (props) => {
const [isIntroVideoShowing, setIsIntroVideoShowing] = useState(false);
return (
<>
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
<Button
{...props}
width={["max-content"]}
variant="tour"
onClick={() => setIsIntroVideoShowing(true)}
data-test="tour-button"
>
<ThirteenBold
mr={["12px"]}
color="cl_blue"
width={["max-content"]}
letterSpacing={["1px"]}
display={["none", "block"]}
textTransform="uppercase"
>
welcome tour
</ThirteenBold>
<PlayIcon style={{ height: "30px", fill: "#4568F9" }} />
</Button>
</>
);
};
And the test coverage report is complaining that I am not testing both of the onClick events, which change the state.
I've tried two approaches, and both fail.
Approach one was to mock the useState and see if it gets called as I'd have expected it.
This was the test I tried:
const setState = jest.fn();
const useStateMock: any = (initState: any) => [initState, setState];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
expect(button).toHaveLength(1);
button.simulate("click");
expect(setState).toHaveBeenCalled();
This shouldn't even be the final test, as it doesn't check what was the valuee it was called with, but still, it failed because useState wasn't even called.
The second approach I've tried was to check the prop value on this component:
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
And this is the test I've tried
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
This one failed claiming it was called with false even after the click.
Is there a way to make these work? Or a different approach to cover these?
You need to give wrapper.update for updating the template with state changes after simulating the click event.
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
wrapper.update();
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
Reference - https://enzymejs.github.io/enzyme/docs/api/ShallowWrapper/update.html

Jest testing library - multiple calls to load data on click event

I am trying to test a load more button call on an onClick fireEvent but I am having trouble simulating the click to trigger a load data.
component:
class Items extends Component {
// states
componentDidMount() {
this.getData()
}
getData() { ...
// get data from state - pagination # and data size
}
onLoadMore() {
// increment pagination & offset on states
this.getData()
}
render() {
return (
<div className='container'>
{items.map((item, i) => {
return (
<div className='item-box'>
// item info
</div>
)
}
)}
<button onClick={this.onLoadMore}>Load More</button>
</div>
)
}
}
test:
it('load more data on load more button click', () => {
const Items = require('./default').default
// set initial load values: initVals (2 items)
// set second call values: secondVals (4 items)
Items.prototype.getData = jest.fn()
Items.prototype.getData.mockReturnValue(initVals)
Items.prototype.getData.mockReturnValue(secondVals)
const { container } = render(
<Items
fields={{ loadMore: true }}
/>
)
const button = screen.getByText('Load More')
fireEvent.click(button)
expect(container.querySelectorAll('.item-box').length).toBe(2)
expect(container.querySelectorAll('.item-box').length).toBe(4)
})
So this only reads the last call, finding 4 items.
Calling .mockReturnValue() multiple times has only yielded me the last call instead of it consecutively. I know I am using it wrong but I can't figure out the sequence of running this. My goal is to initialize the component with first values (load 2 items), then on click, it loads more (4 items).
Help?
I think you need to call mockReturnValueOnce instead of mockReturnValue, and you definitely need to move your first assertion before clicking the event. Also, I think your second assertion should expect 6, not 4.
The order of operations for your test should be:
Set up mocks
Render component
Assert initial value on load
Simulate click event
Assert value after click.
Here is a simple example that demonstrates this concept:
// src/Demo.js
import React, { useState } from "react";
const Demo = ({ loadText }) => {
const [text, setText] = useState("");
return (
<div>
<button onClick={() => setText(loadText())}>Load</button>
<p data-testid="data">{text}</p>
</div>
);
};
export default Demo;
// src/Demo.test.js
import { fireEvent, render, screen } from "#testing-library/react";
import '#testing-library/jest-dom'
import Demo from "./Demo";
test("callbacks", () => {
const myMock = jest.fn();
myMock.mockReturnValueOnce(2);
myMock.mockReturnValueOnce(4);
render(<Demo loadText={myMock} />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("2");
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("4");
});
Also, the way you are mocking this function (Items.prototype.getData = jest.fn()) seems odd to me. I recommend you explore other options, such as (1) mocking out axios or similar library, (2) mocking out a redux store, or (3) mocking out props that you pass to this component.

Testing if button changes state, or if component appears (React)

I have a component with a button and a form. When button is visible, the form is hidden and the opposite - when we click button it dissapears and form is shown. I would like to test it either with enzyme or testing-library, but all my tests fail.
import React, { useState } from 'react';
import Form from './Form';
const FormComponent = () => {
const [isFormVisible, setFormVisibility] = useState(false);
function toggleFormVisibility() {
setFormVisibility(!isFormVisible);
}
return (
<section>
{!isFormVisible && (
<button
id='toggle-form-button'
data-testid='toggle-form-button'
onClick={toggleFormVisibility}
>
Show form
</button>
)}
{isFormVisible && <Form onCancel={toggleFormVisibility} />}
</section>
);
};
export default FormComponent;
My test:
describe('Form component', () => {
it('should fire toggle form action on button click', () => {
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(() => [undefined, setState]);
const component = render(
<Form />
);
const showFormButton = component.getByTestId('toggle-form-button');
Simulate.click(showFormButton);
expect(showFormButton).toBeCalled();
});
});
and another one:
it('should fire toggle form action on button click', () => {
const toggleFormVisibility = jest.fn();
const component = render(
<Form />
);
const showFormButton = component.getByTestId('toggle-form-button');
Simulate.click(showFormButton);
expect(toggleFormVisibility).toBeCalled();
});
It looks like in your tests, you are trying to render the <Form> instead of the <FormComponent>, that might be causing the problem in your test.
Also in your 2nd test, you are not setting up the toggleFormVisibility mocked function with your component, so that wouldn't be invoked at all, the answer above is pretty reasonable, you might want to consider giving that a shot, not sure why it gets downvoted.
testing-library may make this test easier:
import { render, fireEvent } from '#testing-library/react'
render(<Form />);
fireEvent.click(screen.getByLabelText('Show form'));

ReactJS hooks useContext issue

I'm kind of to ReactJS and I'm trying to use useContext with hooks but I'm having some trouble. I've been reading through several articles but I could not understand it.
I understand its purpose, but I can't figure out how to make it work properly. If I'm correct, the purpose is to be able to avoid passing props down to every children and be able to access values from a common provider at any depth of the component tree. This includes functions and state values. Please correct me if I'm wrong.
I've been testing with the following files. This is the ManagerContext.js file:
import { createContext } from 'react';
const fn = (t) => {
console.log(t);
}
const ctx = createContext({
title: 'This is a title',
editing: false,
fn: fn,
})
let ManagerContext = ctx;
export default ManagerContext;
Then I have the LessonManager.js file which is used in my main application:
import React from 'react';
import LessonMenu from './LessonMenu.js';
export default function LessonManager() {
return (
<LessonMenu />
)
}
And finally the LessonMenu.js:
import React from 'react';
import 'rsuite/dist/styles/rsuite.min.css';
import ManagerContext from './ManagerContext.js';
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button
onClick={()=>value.fn('ciao')}
>click</button>
<button
onClick={()=>value.title = 'new title'}
>click</button>
</div>
)
}
In the LessonMenu.js file the onClick={()=>value.fn('ciao')} works but the onClick={()=>value.title = 'new title'} doesn't re render the component.
I know something is wrong, but can someone make it a bit clearer for me?
In order for rerendering to occur, some component somewhere must call setState. Your code doesn't do that, so no rendering happens.
The setup you've done for the ManagerContext creates a default value, but that's only going to get used if you don't render any ManagerContext.Provider in your component tree. That's what you're doing now, but it's almost certainly not what you want to. You'll want to have some component near the top of your tree render a ManagerContext.Provider. This component can will be where the state lives, and among the data it sends down will be a function or functions which set state, thus triggering rerendering:
export default function LessonManager() {
const [title, setTitle] = useState('SomeOtherTitle');
const [editing, setEditing] = useState(false);
const value = useMemo(() => {
return {
title,
setTitle,
editing,
setEditing,
log: (t) => console.log(t)
}
}, [title, editing]);
return (
<ManagerContext.Provider value={value} >
<LessonMenu />
</ManagerContext.Provider/>
)
}
// used like:
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button onClick={() => value.log('ciao')}>
click
</button>
<button onClick={() => value.setTitle('new title')}>
click
</button>
</div>
)
}

Resources