I want to test that a search box does calls a handler (passed as prop) with the fetched results and resets the input field afterwards.
import React, { useState } from 'react'
import Axios from 'axios'
import './style.css'
function SearchBox({ setPhotos }) {
const [searchTerm, setSearchTerm] = useState('')
const handleTyping = (event) => {
event.preventDefault()
setSearchTerm(event.currentTarget.value)
}
const handleSubmit = async (event) => {
event.preventDefault()
try {
const restURL = `https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=${
process.env.REACT_APP_API_KEY
}&per_page=10&format=json&nojsoncallback=1'&text=${encodeURIComponent(
searchTerm
)}`
const { data } = await Axios.get(restURL)
const fetchedPhotos = data.photos.photo
setPhotos(fetchedPhotos)
setSearchTerm('') // 👈 This is giving trouble
} catch (error) {
if (!Axios.isCancel(error)) {
throw error
}
}
}
return (
<section>
<form action="none">
<input
aria-label="Search Flickr"
placeholder="Search Flickr"
value={searchTerm}
onChange={handleTyping}
/>
<button type="submit" aria-label="Submit search" onClick={handleSubmit}>
<span aria-label="search icon" role="img">
🔍
</span>
</button>
</form>
</section>
)
}
export default SearchBox
import React from 'react'
import { render, fireEvent, waitFor, screen } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import SearchBox from '.'
import { act } from 'react-dom/test-utils'
const fakeServer = setupServer(
rest.get(
'https://api.flickr.com/services/rest/?method=flickr.photos.search',
(req, res, ctx) =>
res(ctx.status(200), ctx.json({ photos: { photo: [1, 2, 3] } }))
)
)
beforeAll(() => fakeServer.listen())
afterEach(() => fakeServer.resetHandlers())
afterAll(() => fakeServer.close())
...
test('it calls Flickr REST request when submitting search term', async () => {
const fakeSetPhotos = jest.fn(() => {})
const { getByRole } = render(<SearchBox setPhotos={fakeSetPhotos} />)
const inputField = getByRole('textbox', { name: /search flickr/i })
const submitButton = getByRole('button', { name: /submit search/i })
userEvent.type(inputField, 'Finding Walley')
fireEvent.click(submitButton)
waitFor(() => {
expect(fakeSetPhotos).toHaveBeenCalledWith([1, 2, 3])
waitFor(() => {
expect(inputField.value).toBe('')
})
})
})
This is the error:
Watch Usage: Press w to show more.
● Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in SearchBox (at SearchBox/index.test.js:38)".
38 | aria-label="Search Flickr"
39 | placeholder="Search Flickr"
> 40 | value={searchTerm}
| ^
41 | onChange={handleTyping}
42 | />
43 | <button type="submit" aria-label="Submit search" onClick={handleSubmit}>
waitFor returns a Promise so you need to use await:
await waitFor(() => {
expect(fakeSetPhotos).toHaveBeenCalledWith([1, 2, 3]);
expect(inputField.value).toBe('');
});
Related
I'm trying to do a test, in which when changing the input, I have to read the useMemo and change the disabled of my button, the userEvent is not making this change, has anyone gone through this?
I'm going to put part of my source code here, where the component and the test script are.
<>
<input
data-testid="ipt-email"
value={form.email}
onChange={(e) => {
setForm({ ...form, email: e.target.value });
}}
/>
<button data-testid="submit-sendUser" disabled={isDisabled}>
OK
</button>
</>
This is my hook
const isDisabled = useMemo(() => {
const { email } = form;
if (!email.length) return true
return false;
}, [form]);
Right after that is my unit test, where I write to the input and wait for the state to change
import userEvent from "#testing-library/user-event";
it("Should enable button when form is valid", async () => {
const wrapper = render(<MyComponent />);
const getEmail = wrapper.getByTestId("ipt-email");
await userEvent.type(getEmail, 'example#example.com');
const getBtnSubmit = wrapper.getByTestId("submit-sendUser");
console.log(wrapper.container.innerHTML);
expect(getBtnSubmit).not.toBeDisabled();
});
I can't make the input change reflect in the button hook
Need to wait for changes to occur after the action
await waitFor(() => expect(getBtnSubmit).not.toBeDisabled())
moving code inside the handler applieds to useEffect, but I feel its better to handle the validation inside the handler to so that code changes remain in one place.
Hope it helps
The combination of fireEvents and forced move to the "next frame" worked for me
const tick = () => {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
};
// async test
fireEvent.change(getEmail, {target: {value: 'example#example.com'}})
await tick();
expect(getBtnSubmit).not.toBeDisabled();
In your constant getEmail you get a component with a data-testid='ipt-name' instead of 'ipt-email' (but this is no longer relevant since the requester has modified his question...). The code below works for me :
my test :
import { render, screen, waitFor } from '#testing-library/react';
import App from './App';
import userEvent from '#testing-library/user-event';
it("Should enable button when form is valid", async () => {
render(<App />);
expect(screen.getByTestId("submit-sendUser")).toBeDisabled();
const getEmail = screen.getByTestId("ipt-email");
userEvent.type(getEmail, 'example#example.com');
await waitFor(() => expect(screen.getByTestId("submit-sendUser")).not.toBeDisabled());
});
my component :
import React, { useMemo, useState } from "react";
export const App = () => {
const [form, setForm] = useState({ email: '' });
const isDisabled = useMemo(() => {
const { email } = form;
if (!email || !email.length) return true;
return false;
}, [form]);
return (
<div>
<input
data-testid="ipt-email"
value={form.email}
onChange={(e) => {
setForm({ ...form, email: e.target.value });
}}
/>
<button data-testid="submit-sendUser" disabled={isDisabled}>
OK
</button>
</div>
);
};
export default App;
Hi have this simple piece of code inspired by https://testing-library.com/docs/example-react-formik/
import React from "react";
import { Formik, Field, Form } from "formik";
const sleep = (ms: any) => new Promise((r) => setTimeout(r, ms));
export const MyForm = () => {
const handleSubmit = async (values: any) => {
await sleep(500);
console.log(values);
};
return (
<div>
<Formik
initialValues={{
firstName: "",
}}
onSubmit={handleSubmit}
>
<Form>
<label htmlFor="firstName">First Name</label>
<Field id="firstName" name="firstName" placeholder="Jane" />
<button type="submit">Submit</button>
</Form>
</Formik>
</div>
);
};
and the Test:
import React from "react";
import { render, screen, waitFor } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import { MyForm } from "./MyForm";
test("rendering and submitting a basic Formik form", async () => {
const handleSubmit = jest.fn(); // this doing nothing
render(<MyForm />);
const user = userEvent.setup();
await user.type(screen.getByLabelText(/first name/i), "John");
await user.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
});
Console.log printed the inputed value: { firstName: 'John' }, but the test fails due the fact it understand that handleSubmit was not been called.
What’s going wrong with this code?
Because you didn't pass the mock handleSubmit to <MyForm/> component. You should pass it to the component as onSubmit prop and call it when the internal handleSubmit event handler executes.
Let's see the RTL official formik testing example
MyForm.tsx:
export const MyForm = ({onSubmit}) => {
const handleSubmit = async values => {
await sleep(500)
submit(values)
}
return <div>...</div>
}
MyForm.test.tsx:
test('rendering and submitting a basic Formik form', async () => {
const handleSubmit = jest.fn();
render(<MyForm onSubmit={handleSubmit} />);
await user.click(screen.getByRole('button', {name: /submit/i}));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
})
Did you see the difference between the official example and your code?
I'm trying to create a edit form to edit data from database by id. I tries this:
import React, {FormEvent, useEffect, useState} from "react";
import TextField from "#material-ui/core/TextField";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import {
TicketFullDTO,
TicketStatusTypesDTO,
} from "../../service/support/types";
import {
getTicket,
getTicketStatusTypes,
updateTicket,
} from "../../service/support";
import { useHistory, useParams } from "react-router-dom";
import InputLabel from "#mui/material/InputLabel";
import Select from "#mui/material/Select";
import MenuItem from "#mui/material/MenuItem";
import { FormControl } from "#mui/material";
import { Moment } from "moment";
import { RouteParams } from "../../service/utils";
export default function TicketProfile(props: any) {
const classes = useStyles();
let history = useHistory();
let requestParams = useParams<RouteParams>();
const [status, setStatus] = useState<string>("");
const [submitDate, setSubmitDate] = useState<Moment | null>(null);
// This won't be run unless all the input validations are met.
const onSubmit = async (data: TicketFullDTO) => {
console.log(data);
updateTicket(requestParams.id, data)
.then(({ data }) => {
console.log(data.title);
history.replace("/support");
})
.catch((err) => {
console.log(err);
});
};
const [ticketCategoriesList, setTicketCategoriesList] = useState<
TicketCategoryTypesDTO[]
>([]);
const [ticket, setTicket] = useState<TicketFullDTO>();
useEffect(() => {
getSingleTicket();
}, []);
const getSingleTicket = async () => {
getTicket(requestParams.id)
.then(({ data }) => {
setTicket(data);
})
.catch((error) => {
console.error(error);
});
};
const [ticketStatusList, setTicketStatusList] = useState<
TicketStatusTypesDTO[]
>([]);
useEffect(() => {
ticketStatusData();
}, []);
const ticketStatusData = async () => {
getTicketStatusTypes()
.then((resp) => {
setTicketStatusList(resp.data);
})
.catch((error) => {
console.error(error);
});
};
return (
<Container>
<form onSubmit={onSubmit}>
.........
<TextField
value={ticket?.title}
id="title"
onChange={({ target: { value } }) => {
setTicket({ ...ticket, title: value });
}}
/>
.........
<FormControl>
<TextField
label="Submit Date"
id="submit-date"
type="date"
defaultValue={ticket?.submitDate}
//#ts-ignore
onInput={(e) => setSubmitDate(e.target.value)}
/>
</FormControl>
..........
<Select
labelId="status-label"
id="status-helper"
value={ticket?.status}
onChange={(e) => setStatus(e.target.value)}
required
>
{ticketStatusList.map((element) => (
<MenuItem value={element.code}>
{element.name}
</MenuItem>
))}
</Select>
</FormControl>
...........
<Button
type="submit"
>
Update Ticket
</Button>
</Container>
);
}
.....
export async function updateTicket(
id: string,
data: TicketFullDTO
): Promise<AxiosResponse<TicketFullDTO>> {
return await axios.post<TicketFullDTO>(
`${baseUrl}/management/support/tickets/ticket/${id}`,
{
data,
}
);
}
export interface TicketFullDTO {
id?: number,
title?: string,
status?: string,
submitDate?: Moment | null
}
I at this line: <form onSubmit={onSubmit}> I get this error:
TS2322: Type '(data: TicketFullDTO) => Promise<void>' is not assignable to type 'FormEventHandler<HTMLFormElement>'. Types of parameters 'data' and 'event' are incompatible. Type 'FormEvent<HTMLFormElement>' has no properties in common with type 'TicketFullDTO'. index.d.ts(1390, 9): The expected type comes from property 'onSubmit' which is declared here on type 'DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>'
Do you know how I can fix this issue?
It looks like what you want to use is the ticket state variable as your data since all your fields are updating that state variable, which would be done as follow:
const onSubmit = async () => {
updateTicket(requestParams.id, ticket)
.then(({ data }) => {
console.log(data.title);
history.replace("/support");
})
.catch((err) => {
console.log(err);
});
};
The reason why your type validation is failing is because the onSubmit event handler gets the event as a parameter, so if you do want to make use of the parameter, you would do the following (where e would be the event object):
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
...
my problem is that I have two different components belonging to my App.js project. It's a movie database where I have a list of movies on the front page and I can search for other movies using the search bar. Since I have the search.js and movie.js ( component where i fetch api data and display), the search.js will not trigger as it cant pinpoint what needs to change. Basically my problem is that on submit, nothing changes.
search.js code:
import { useState } from 'react';
import React from 'react';
// search API used to search through database
const searchUrl = "https://api.themoviedb.org/3/search/movie?api_key=d62e1adb9803081c0be5a74ca826bdbd&query="
const Search = ({ }) => {
const [movies, setMovies] = useState([]);
const [search, setSearch] = useState("");
// Search form that fetches search API and returns results
const submitForm = (e) => {
e.preventDefault();
// API used to search for any movie in the database
fetch(searchUrl + search)
.then(res => res.json())
.then(data => {
setMovies(data.results);
})
setSearch("");}
// user search input
const searchQuery = (e) => {
setSearch(e.target.value)
}
return (
<form onSubmit={submitForm}>
<i class="fas fa-search"></i>
<label className="sr-only" htmlFor="searchMovie">Search for a movie</label>
<input
className="search"
type="search"
placeholder="Search for a movie.."
value={search}
onChange={searchQuery}
/>
</form>
)
}
export default Search;
and my movie.js
import { Link } from 'react-router-dom';
import { useState, useEffect } from "react";
const images = "https://image.tmdb.org/t/p/w500/";
// main API used to display trending page
const apiUrl = `https://api.themoviedb.org/3/movie/now_playing?api_key=d62e1adb9803081c0be5a74ca826bdbd&page=`;
const Movie = ( {
}) => {
const [movies, setMovies] = useState([]);
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then((data)=> {
setMovies(data.results)
})
}, []);
return (
<section className="movieslist">
{movies.length > 0 ? movies.map((movie) => {
return (
<Link to={`/movie/${movie.id}`}>
<div className="moviePoster">
<img src={movie.poster_path ? `${images}${movie.poster_path}` : "https://www.movienewz.com/img/films/poster-holder.jpg"} alt={movie.title} />
<div className="movieInfo">
<h2>{movie.title}</h2>
<p className="voteStyle">Rating: {movie.voteAverage}</p>
<p className="release">Release Date: {movie.release}</p>
<p className="summary">{movie.overview}</p>
<p className="key">{movie.id}</p>
</div>
</div>
</Link>
);
}): <p class="noResults">No results found. Please try again?</p>}
</section>
)
}
export default Movie;
If I understand the expected behavior correctly, you're trying to update the movies state in movies.js from the search.js.
You are updating two different states of two different components that have no relationship with themselves and that is why nothing is happening on submit.
What you'll need is a parent component (for example home.js) that holds search and movies component as children and holds the movies state. The child components should use and update the parent's movie state.
import Movies from "./movies";
import Search from "./search";
const Home = ()=>{
const [movies, setMovies] = useState([]);
// some other code
return (
<>
<Search onSearh={setMovies} />
<Movies movies={movies} onMovies={setMovies}/>
</>);
}
and your movies.js and search.js should consume these props
import { useState } from 'react';
import React from 'react';
// search API used to search through database
const searchUrl = "https://api.themoviedb.org/3/search/movie?api_key=d62e1adb9803081c0be5a74ca826bdbd&query="
const Search = ({ onSearch }) => {
const [search, setSearch] = useState("");
// Search form that fetches search API and returns results
const submitForm = (e) => {
e.preventDefault();
// API used to search for any movie in the database
fetch(searchUrl + search)
.then(res => res.json())
.then(data => {
onSearch(data.results);
})
setSearch("");}
...
import { Link } from 'react-router-dom';
import { useState, useEffect } from "react";
const images = "https://image.tmdb.org/t/p/w500/";
// main API used to display trending page
const apiUrl = `https://api.themoviedb.org/3/movie/now_playing?api_key=d62e1adb9803081c0be5a74ca826bdbd&page=`;
const Movie = ( {movies, onMovies}) => {
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then((data)=> {
onMovies(data.results)
})
}, []);
...
i am getting an error "400 bad request" i can't find out what is my error
below i share my frontend and backend code..
i am also share my image error link that i came
https://ibb.co/swQPgYG
################### backend ######################
todoschema.js
this is a todoschema
var mongoose = require('mongoose');
var todoSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength:32,
trim:true
}
},{timestamps:true}
)
module.exports = mongoose.model('Todo',todoSchema)
auth.js/router
var express = require('express');
var router = express.Router();
const {addTodo} = require('../controller/auth');
router.post('/addtodo',addTodo);
module.exports = router;
auth.js/controller
const Todo = require('../model/todo');
exports.addTodo = (req,res) =>{
const todo = new Todo(req.body)
todo.save((err,todo) => {
if(err || !todo){
return res.status(400).json({
err : 'NOT able to store data in database'
})
}
res.json(todo);
})
}
################## frontEnd ###########################
API == http://localhost:8000/api/
here i fetch request from my backend
index.js
import {API} from '../backend'
import axios from 'axios'
export const getdata = (todo) => {
return (dispatch) => {
axios.post(`${API}addtodo`)
.then(res => {
console.log(res)
dispatch({
type : 'FETCH_TODO',
payload : todo
})
})
.catch(err =>{
console.log(err);
})
}
}
This is the todoForm where i add my todo
todoform.js
import React,{useState, useEffect} from 'react'
import '../App.css'
import {
FormGroup,
Input,
Button,
Form,
InputGroup,
InputGroupAddon
} from 'reactstrap';
import {v4} from 'uuid';
import 'bootstrap/dist/css/bootstrap.min.css';
import {getdata } from '../Auth'
//redux
import {connect, useDispatch} from 'react-redux'
import {addTodo} from '../Action/todo';
const TodoForm = ({addTodo}) => {
const [title,setTitle] = useState('')
const dispatch = useDispatch();
useEffect(() => {
dispatch(getdata())
}, []);
return(
<Form>
<FormGroup>
<InputGroup>
<Input
type='text'
name='todo'
id='todo'
placeholder='Your next Todo'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<InputGroupAddon addonType='prepend'>
<Button color='primary' onClick={()=>{
if(title === ''){
return alert('please add a todo')
}
const todo = {
title,
id:v4(),
}
addTodo(todo);
setTitle('');
}}>
ADD
</Button>
</InputGroupAddon>
</InputGroup>
</FormGroup>
</Form>
)
}
const mapStateToProps = state => ({
})
const mapDispatchToProps = dispatch =>({
addTodo : todo =>{
dispatch(addTodo(todo))
},
})
export default connect(mapStateToProps,mapDispatchToProps)(TodoForm)
getData(todo) - action creator function require argument
export const getdata = (todo) => {
return (dispatch) => {
axios.post(`${API}addtodo`)
.then(res => {
console.log(res)
dispatch({
type : 'FETCH_TODO',
payload : todo
})
})
.catch(err =>{
console.log(err);
})
}
}
and you call it without argument
const dispatch = useDispatch();
useEffect(() => {
dispatch(getdata())
}, []);