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?
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;
I am developing using react and antd.
This is useAxios I wrote.
import { useState, useEffect } from 'react';
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:3000/';
const useAxios = ({ url, method, body = null, headers = null }) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState('');
const [loading, setloading] = useState(true);
const fetchData = () => {
axios[method](url, headers, body)
.then((res) => {
setResponse(res.data);
})
.catch((err) => {
setError(err);
})
.finally(() => {
setloading(false);
});
};
useEffect(() => {
fetchData();
}, [method, url, body, headers]);
return { response, error, loading };
};
export default useAxios;
This is the login component.
import React from 'react';
import { Form, Input, Button } from 'antd';
import useAxios from '../hooks/useAxios';
const LoginForm = () => {
const axios = useAxios;
const onFinish = (body) => {
const test = axios({
url: 'api/auth/login',
method: 'post',
body,
});
console.log(test);
};
const findIDAndPassword = () => {
console.log('findIDAndPassword');
};
return (
<Form name="basic" onFinish={onFinish} autoComplete="off">
<Form.Item
name="id"
rules={[
{
required: true,
message: 'Please input your id!',
},
]}
>
<Input placeholder="ID" />
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password placeholder="PASSWORD" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
login
</Button>
</Form.Item>
<Button type="link" block onClick={findIDAndPassword}>
findIDAndPassword
</Button>
</Form>
);
};
export default LoginForm;
But it gives me this error. How to fix?
src\components\LoginForm.js
Line 7:18: React Hook "useAxios" is called in function "onFinish" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.
it seems to me that the hook call is invalid.
Here's a quick implementation of what I think you are doing, but it is by no means a perfect solution.
Read more about customs hooks: React Documentation
In your useAxios hook:
export const useAxios = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = ({url, headers, body, method}) => {
return axios[method](url, headers, body)
.then((res) => {
setResponse(res.data);
})
.catch((err) => {
setError(err);
})
.finally(() => {
setLoading(false);
});
}
return { fetchData, response, error, loading };
};
And in your LoginForm:
const { fetchData, response, error, loading } = useAxios();
const onFinish = (body) => {
return fetchData({
url: 'api/auth/login',
method: 'post',
body,
headers: {
//...headers
}
});
};
I was trying to set my value in the input value! but after that, I cannot write anything in the input field! I wanted to set values from the back end in value!
We are writing an admin channel to edit the article for that we need already existing article values to edit the article! What am I doing wrong! or Maybe you can suggest a better way to edit the article in the admin channel!
here is the code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router';
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
const [changedValues, setChangedValues] = useState('');
console.log('values', editValues);
console.log('changed', changedValues);
const params = useParams();
console.log(params);
const resultsId = params.id;
console.log('string', resultsId);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem('token') || ''
);
const setTokens = (data) => {
localStorage.setItem('token', JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(
`${process.env.REACT_APP_API_URL}/article/${resultsId}`
);
setEditValues(res.data);
} catch (err) {}
};
fetchData();
}, [resultsId]);
const inputValue = editValues;
const userToken = props.token;
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ''}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<input
// ref={editValues.shortDesc}
value={editValues.shortDesc}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<button type='submit'>send</button>
</form>
</div>
);
};
export default EditArticle;
your onChange handler is updating a different state property than what is being used as the value on the input (editValues vs changedValues).
Also you can pass a defaultValue to input that will get used as the default value only.
See more here https://reactjs.org/docs/uncontrolled-components.html
you can use just do it just using editValues. try this:
I just reproduced it without the api call to run the code.
import React, { useState, useEffect } from "react";
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
console.log("values", editValues);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem("token") || ""
);
const setTokens = (data) => {
localStorage.setItem("token", JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
//here get the data from api and setstate
setEditValues({ title: "title", shortDesc: "shortDesc" });
} catch (err) {}
};
fetchData();
}, []);
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ""}
onChange={(input) => setEditValues({title: input.target.value})}
type="text"
/>
<input
value={editValues.shortDesc}
onChange={(input) => setEditValues({shortDesc: input.target.value})}
type="text"
/>
<button type="submit">send</button>
</form>
</div>
);
};
export default EditArticle;
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('');
});
I recently started testing with Jest and react-testing-library. But I ran into a little problem. I am testing my component which looks like this:
import React, { useState } from 'react';
const Login = () => {
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
};
return (
<div>
<form onSubmit={handleSubmit} data-testid="loginForm">
<label>
Username:
<input
required
type="text"
data-testid="usernameText"
placeholder="e.g. JohnLukeThe3rd"
value={username}
onChange={(event) => setUsername(event.target.value)}
autoFocus
/>
</label>
<label>
Password:
<input
required
type="password"
data-testid="passwordText"
placeholder="e.g. β’β’β’β’β’β’β’β’β’β’"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
</label>
<input type="submit" data-testid="loginButton" value="Login" />
</form>
</div>
);
};
export default Login;
As far as I understand onSubmit is not called when form is submitted but form's required input fields are empty. I checked this behavior by inserting a console.log('Hey') in handleSubmit but nothing got printed. So, I thought that in my test the same should happen. When I check if submit handler doesn't get called when input is empty it should succeed. But it doesn't. It fails.
Here's the testing code:
import React from 'react';
import {
render,
fireEvent,
cleanup,
RenderResult,
} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import Login from '../components/Login';
describe('<Login />', () => {
let screen: RenderResult;
beforeEach(() => {
screen = render(<Login />);
});
afterEach(cleanup);
describe('typing user info', () => {
it('shows username and password', async () => {
expect(screen.queryByDisplayValue('JohnLukeThe3rd')).toBeNull();
await userEvent.type(
screen.getByLabelText(/username/i),
'JohnLukeThe3rd',
);
await userEvent.type(screen.getByLabelText(/password/i), 'Password123');
expect(screen.queryByDisplayValue('JohnLukeThe3rd')).toBeInTheDocument();
expect(screen.queryByDisplayValue('Password123')).toBeInTheDocument();
});
});
describe('clicking login button', () => {
let loginButton: HTMLElement;
let submitHandler: jest.Mock;
let form: HTMLElement;
beforeEach(() => {
loginButton = screen.getByText(/login/i);
submitHandler = jest.fn((e) => e.preventDefault());
form = screen.getByTestId(/loginForm/i);
form.onsubmit = submitHandler;
});
// problem area here
it('does not call submit handler if fields empty', () => {
expect(submitHandler).not.toHaveBeenCalled();
userEvent.click(loginButton);
expect(submitHandler).not.toHaveBeenCalled();
});
// problem area ends
it('calls submit handler if fields non-empty', async () => {
expect(submitHandler).not.toHaveBeenCalled();
const usernameField = screen.getByTestId('usernameText');
const passwordField = screen.getByTestId('passwordText');
await userEvent.type(usernameField, 'JohnLukeThe3rd');
await userEvent.type(passwordField, 'Password123');
await userEvent.click(loginButton);
expect(submitHandler).toHaveBeenCalledTimes(1);
});
});
});
The test 'does not call submit handler if fields empty' fails. What am I doing wrong? I couldn't find any information on testing form with required input fields anywhere. Am I doing it wrong? Thanks.
Also, just so you know: I did screen.debug() in the test and both input's value was 'value=""'