How to test with Jest and Enzyme a custom form hook? - reactjs

I'm new doing automation testing, so I'm trying to do some unit tests, I built a usual custom hook to handle the form events and the inputs changes.
import { useState } from 'react'
const useFormHook = (callback) => {
const [inputs, setInputs] = useState({});
const handleSubmit = (e) => {
if (e) {
e.preventDefault();
}
setInputs({})
callback();
}
const handleInputChange = (e) => {
e.persist();
setInputs(inputs => ( {...inputs, [e.target.name]: e.target.value } ));
}
return{
handleSubmit,
handleInputChange,
inputs
}
}
export default useFormHook
and the implementation goes in to a search bar, which event will update a context state, and looks like this:
const SearchBar = () => {
const [search, setSearch] = useState('')
useAsyncHook(search); // I'm using an async custom hook to fetch some data with axios.
const handleFormSubmit = () => {
let targetValue = Object.values(inputs).join().toLowerCase()
setSearch(targetValue)
document.forms[0].reset()
}
const { inputs, handleSubmit, handleInputChange } = useCustomFormHook(handleFormSubmit)
return (
<form onSubmit={handleSubmit} className="searchForm">
<input
id='searchBar'
type="text"
name="value"
value={inputs.value || ''}
onChange={handleInputChange}
required
className={`input`}
/>
<input
className={`button primary`}
type="submit"
value="πŸ” Search"
id='search-button'
/>
</form>
)
}
till now this work fine..:
import React from "react";
import { mount, shallow } from "enzyme";
//components and hooks
import useFormHook from "../../../Services/Hooks/customFormHook";
//import useAsyncHook from "../../../Services/Hooks/customAsyncHook";
//import SeachBar from "../SearchBar";
describe("Custom hooks suite test", () => {
// const wrapper = shallow(<SeachBar />);
let results;
const handleHookTester = (hook) => {
function HookWrapper() {
results = hook();
return null;
}
mount(<HookWrapper />);
return results;
};
it("Form hook test", () => {
handleHookTester(useFormHook);
expect(results.inputs).toStrictEqual({});
});
});
So far I've managed to make it work, but this is just the customFormHook and I'd like to test and my customAsyncHook which looks like this:
const useAsyncHook = (id) => {
const [loading, setLoading] = useState('false');
const { findData } = useContext(AppContext)
// 1 -
useEffect(() => {
async function getData() {
try {
setLoading('true');
const response = await axios(
`someEndPoint/${id}`
);
findData({type:FIND_DATA, data:response.data });
} catch (error) {
setLoading('null');
findData({type:FIND_DATA, data:null });
}
}
if (id !== "") {
getData();
}
}, [id]);
return loading;
}
and if I try something like const wrapper = shallow(<SearchBar/>) or:
handleHookTester(useAsyncHook); the test fails throwing the following error:
● Custom hooks suite test β€Ί encountered a declaration exception
TypeError: Cannot destructure property 'findData' of '(0 , _react.useContext)(...)' as it is undefined.
8 | const [loading, setLoading] = useState('false');
9 |
> 10 | const { findData } = useContext(AppContext)
| ^
11 | // 1 -
12 | useEffect(() => {
So the question is should I mock that function? and how?

Related

How to pass a value from an input to a submit button?

I'm currently working on a project to implement a website to check the weather forecast.
I'm trying to get the value from the input field and when I click the submit button, this value should be set to cityName. What do I have to change in order to make this work?
import { useState, useEffect } from "react"
export function WeatherInfo() {
const token: string = '7ebe7c2a03cd48c090a193437'
async function getCurrentWeather(cityName: string): Promise<any> {
const response = await fetch(`http://api.weatherapi.com/v1/current.json?key=${token}&q=${cityName}`)
const data = await response.json()
console.log(data)
return data
}
const [cityName, setCityName]: any = useState('')
const [cityWeather, setCityWeather] = useState({})
const [value, setValue] = useState('')
const handleChange = (event: any) => {
setValue(event.target.value)
}
const handleSubmit = (event: any) => {
event.preventDefault()
setCityName(value)
}
useEffect(() => {
async function fetchData() {
const cityWeather = await getCurrentWeather(cityName)
}
fetchData()
})
return (
<div >
<form onSubmit={handleSubmit}>
<input onChange={handleChange} placeholder="Type here" />
<button>Search</button>
</form>
</div>
);
}
You should add a dependency array to your effect hook so that it triggers whenever cityName changes.
Updating the cityWeather state should only be done via the setCityWeather function.
useEffect(() => {
if (cityName) { // only fetch when you've got a value
getCurrentWeather(cityName).then(setCityWeather);
}
}, [cityName]);
You should also try to use as few any types as possible, preferably none
// define stand-alone functions outside your components
// eg weather-api.ts
const token = "your-api-key";
export interface CurrentWeather {
temp_c: number;
feelslike_c: number;
// etc
}
export async function getCurrentWeather(
cityName: string
): Promise<CurrentWeather> {
// safely encode URL query params
const params = new URLSearchParams({
key: token,
q: cityName,
});
const response = await fetch(
`http://api.weatherapi.com/v1/current.json?${params}`
);
// don't forget to check for errors
if (!response.ok) {
throw response;
}
return response.json(); // will be cast to the `CurrentWeather` type
}
import { useState, useEffect, FormEventHandler } from "react";
import { getCurrentWeather, CurrentWeather } from "./weather-api";
export function WeatherInfo() {
const [cityName, setCityName] = useState("");
const [cityWeather, setCityWeather] = useState<CurrentWeather>(); // default undefined
const [value, setValue] = useState("");
useEffect(() => {
getCurrentWeather(cityName).then(setCityWeather).catch(console.error);
}, [cityName]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setCityName(value);
};
return (
<div>
{cityWeather && (
<p>
The current temperature in {cityName} is {cityWeather.temp_c} Β°C
</p>
)}
<form onSubmit={handleSubmit}>
<input
onChange={(e) => setValue(e.target.value)}
placeholder="Type here"
/>
<button>Search</button>
</form>
</div>
);
}

Get input value with react hooks to search on oMdb api

I want to do a movie search with the oMdb api using React Hooks.
The result is not as expected. I seem to break some React Hooks rule that I don't understand.
Here is the code.
HOOK TO SEARCH
The Hook inside of a store.
(If I use searchMovies('star wars') in a console.log I can see the result of star wars movies and series.)
import React, { useState, useEffect } from "react";
const useSearchMovies = (searchValue) => {
const API_KEY = "731e41f";
const URL = `http://www.omdbapi.com/?&apikey=${API_KEY}&s=${searchValue}`
// Manejador del estado
const [searchMovies, setSearchMovies] = useState([])
//Llamar y escuchar a la api
useEffect(() => {
fetch(URL)
.then(response => response.json())
.then(data => setSearchMovies(data.Search))
.catch((error) => {
console.Console.toString('Error', error)
})
}, []);
return searchMovies;
};
THE INPUT ON A SANDBOX
Here i have the input to search with a console log to see the result.
import React, { useState } from "react";
import searchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('')
const onChangeHandler = e =>{
setSearch(e.target.value)
console.log('Search result', searchMovies(search))
}
const handleInput =()=> {
console.log('valor del input', search)
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput()}>search</button>
</div>
</div>
)
}
export default Sandbox;
Issue
You are breaking the rules of hooks by conditionally calling your hook in a nested function, i.e. a callback handler.
import searchMovies from "../store/hooks/useSearchMovies";
...
const onChangeHandler = e => {
setSearch(e.target.value);
console.log('Search result', searchMovies(search)); // <-- calling hook in callback
}
Rules of Hooks
Only call hooks at the top level - Don’t call Hooks inside loops,
conditions, or nested functions.
Solution
If I understand your code and your use case you want to fetch/search only when the search button is clicked. For this I suggest a refactor of your useSearchMovies hook to instead return a search function with the appropriate parameters enclosed.
Example:
const useSearchMovies = () => {
const API_KEY = "XXXXXXX";
const searchMovies = (searchValue) => {
const URL = `https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchValue}`;
return fetch(URL)
.then((response) => response.json())
.then((data) => data.Search)
.catch((error) => {
console.error("Error", error);
throw error;
});
};
return { searchMovies };
};
Usage:
import React, { useState } from "react";
import useSearchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('');
const [movies, setMovies] = useState([]);
const { searchMovies } = useSearchMovies();
const onChangeHandler = e => {
setSearch(e.target.value)
};
const handleInput = async () => {
console.log('valor del input', search);
try {
const movies = await searchMovies(search);
setMovies(movies);
} catch (error) {
// handle error/set any error state/etc...
}
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput}>search</button>
</div>
<ul>
{movies.map(({ Title }) => (
<li key={Title}>{Title}</li>
))}
</ul>
</div>
);
};
export default Sandbox;

Update input box when outside is clicked in ReactJS

I am trying to update the database. So I have an input field that is disabled as default. So when you click, editing is enabled and when you click outside of the input field, it gets disabled again. What I am trying to do is update when you click outside of the input field. So, my input is like this:
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={() => {
setDisabled(true);
handleChange(priceValue);
}}
onChange={handleChange(mainPricePosts)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
And this is how I am trying to update but I am not sure if I am doing right to get the value from the input field:
const [updatePosts, setUpdatePosts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [show, setShow] = useState(false);
const [showError, setShowError] = useState(false);
const handleClose = () => setShow(false);
const handleCloseError = () => setShowError(false);
const fetchIndividualPosts = async ({ value, post: { mainPricePosts, key } = {} }) => {
console.log(value);
try {
setLoading(true);
const res = await Axios({
method: "POST",
url: `url`,
headers: {
"content-Type": "application/json",
},
data: {
updated_parameter: ["main_price", "small_car", key],
updated_value: value,
},
});
if (res.status === 200) {
setUpdatePosts(res.data);
}
setLoading(false);
} catch (err) {
console.log(err.response.status);
setError(err.response.data.error);
setLoading(false);
}
};
const handleChange = (mainPricePosts) => (e) => {
fetchIndividualPosts({ mainPricePosts, value: e.target.value });
};
This is also the curl how I can update the data:
curl -L -i -H "Content-Type: application/json" -X POST -d '{
"updated_parameter":["100"],
"updated_value":"0.044"
}' $ip''
so updated_value should be the updated input (the value after, outside is clicked)
100, should be the key of the input value.
Hope it is clear and you can help me about this problem.
Thanks for your help beforehand.
There are many ways you can achieve what you need, but I would use following approach.
In your InputPrice component on onBlur event I would disable input by calling setDisabled(true) and then use useEffect hook to call handleChange callback if new price value and original price values are different. Because you are calling setDisabled(true), you're actually re-rendering your InputPrice component and therefore not executing handleChange callback.
Checkout code below.
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
useEffect(() => {
let callUpdateCallback = false;
if (priceValue !== mainPricePosts) callUpdateCallback = true;
if (disabled && callUpdateCallback) handleChange(priceValue);
}, [disabled, priceValue, handleChange, mainPricePosts]);
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={setDisabled(true)}
onChange={(e) => setPriceValue(e.target.value)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
You call this component like this
import React from "react";
import ReactDOM from "react-dom";
import InputPrice from "./InputPrice";
function App() {
const handleChange = (e) => {
console.log("app handle change", e);
// You can call your fetch here...
};
return (
<div>
<InputPrice mainPricePosts="500" handleChange={handleChange} />
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
Additionally there codesandbox that used to debug it, so if you need more details you can find it on the link below.
https://codesandbox.io/s/reactjs-playground-forked-8vwe2?file=/src/index.js:0-364

How can I test a React Hooks component by changing useState

I have a React hooks functional component that I'd like to test with Jest/Enzyme. I would like test its tertiary render behaviour based upon a useState value. I can't seem to find any example online. There is no 'click' to simulate - no API call to mock because at the end, I still need to test based upon the useState value.
In the past, with class components, I could set the state. With the new hooks, I can't.
So, basically - how do I mock an async await inside a mocked submitForm function so that the render behaves properly?
Here's my component:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'core/Form';
export const Parent = ({submitForm}) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = name => evt => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
Here's my testing so far:
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import Parent from './Parent';
import Form from 'core/Form';
let wrapper, props;
.
.
.
describe('<Parent /> rendering', () => {
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Parent {...props} />);
});
afterEach(() => {
jest.clearAllMocks();
});
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(init => [init, setState]);
it('Should render 1 Form', () => {
expect(wrapper.find(Form)).toHaveLength(1);
});
it('renders Redirect after API call', () => {
setRedirect = jest.fn(() => false);
expect(wrapper.find(Redirect)).toHaveLength(1);
});
it('renders Form before API call', () => {
setRedirect = jest.fn(() => true);
expect(wrapper.find(Form)).toHaveLength(1);
});
});
You don't need to spy useState hook. Which means you should not test these hooks and methods of the component directly. Instead, you should test components' behavior(the state, props and what is rendered)
E.g.
index.tsx:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
export const Form = ({ onSubmit, onChange, values }) => <form onSubmit={onSubmit}></form>;
const path = '/user';
export const Parent = ({ submitForm }) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = (name) => (evt) => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
index.test.tsx:
import Parent, { Form } from './';
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
const whenStable = async () =>
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
describe('60137762', () => {
it('should render Form', () => {
const props = { submitForm: jest.fn() };
const wrapper = shallow(<Parent {...props}></Parent>);
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle submit and render Redirect', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(true) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Redirect)).toBeTruthy();
});
it('should handle submit and render Form', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(false) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle error if submit failure', async () => {
const logSpy = jest.spyOn(console, 'log');
const mError = new Error('network');
const props = { submitForm: jest.fn().mockRejectedValueOnce(mError) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(logSpy).toHaveBeenCalledWith('Submit error: ', mError);
});
});
Unit test results with coverage report:
PASS stackoverflow/60137762/index.test.tsx
60137762
βœ“ should render Form (18ms)
βœ“ should handle submit and render Redirect (15ms)
βœ“ should handle submit and render Form (8ms)
βœ“ should handle error if submit failure (18ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Submit error: Error: network
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:39:20
at step (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:44:23)
at Object.next (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:25:53)
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:19:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:15:12)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:37:47)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 78.57 | 100 | 40 | 93.75 |
index.tsx | 78.57 | 100 | 40 | 93.75 | 12
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 3.716s, estimated 5s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60137762

React Hooks state not updating variable in view

I'm trying to set a state, which I fetch from an API in the form of an array.
Tried this in every way possible, doesn't work.
Any ideas on how to fix this?
instance is an axios.create that creates the instance to a localhost django server which has CORS-ALLOW-CROSS-ORIGIN True
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
fetchText();
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Do it like this,
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
useEffect(() => {
fetchText();
},[ any variable you want it to fetch that again ]);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
This looks to be a good use case for a useEffect hook. Also, you need to await async functions within useEffect statement. The useEffect hook cannot be an async function in itself. Also, the original implementation would result in an infinite loop. The setState function would trigger a re-render which would then trigger the fetch function to fire off again. Consider the following:
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const request= await instance.get(`/one/list/`);
request.then( r => setOne(r.data))
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Based on you guys' advice, I came up with the below code which works fine so far.
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const [el, setEl] = useState(null);
const fetchText = async () => {
let res = await instance.get("/one/list/");
setOne(res.data);
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
useEffect(() => {
const handleClick = e => {
let newEl = document.createElement("input");
newEl.value = e.target.innerHTML;
newEl.id = e.target.id;
newEl.addEventListener("keypress", e => handleInput(e));
newEl.addEventListener("focusout", e => handleInput(e));
e.target.parentNode.replaceChild(newEl, e.target);
newEl.focus();
};
const handleInput = async e => {
console.log(e.type);
if (
e.target.value !== "" &&
(e.key === "Enter" || e.type === "focusout")
) {
let payload = { text: e.target.value };
try {
e.preventDefault();
let res = await instance.put(`/one/update/${e.target.id}`, payload);
let text = res.data.text;
e.target.value = text;
let newEl = document.createElement("span");
newEl.innerHTML = text;
newEl.addEventListener("click", e => handleClick(e));
newEl.id = e.target.id;
e.target.parentNode.replaceChild(newEl, e.target);
} catch (e) {
console.error(e);
}
}
};
setEl(
one.map(o => (
<p>
<span id={o.id} onClick={e => handleClick(e)}>
{o.text}
</span>
</p>
))
);
}, [one]);
return <>{el}</>;
};
export default OneList;

Resources