I'm currently trying a simple login form with an async call in React using TypeScript and classes.
My component looks like this:
import * as React from 'react';
import { LoginService } from './services/LoginService';
interface CredentialsState {
userName: string,
password: string,
isLoggedIn: boolean,
loginAttempted: boolean
}
interface CustomEvent {
target: HTMLInputElement
}
export class Login extends React.Component<{}, CredentialsState> {
state: CredentialsState = {
password: "",
userName: "",
isLoggedIn: false,
loginAttempted: false
};
private loginService: LoginService = new LoginService();
private async handleSubmit(event: React.SyntheticEvent) {
event.preventDefault();
const loginResponse = await this.loginService.login(
this.state.userName,
this.state.password
);
console.log('Login result ' + loginResponse);
this.setState({
loginAttempted: true,
isLoggedIn: loginResponse
});
}
private setPassword(event: CustomEvent) {
this.setState({ password: event.target.value });
}
private setUserName(event: CustomEvent) {
this.setState({ userName: event.target.value });
}
render() {
let loginLabel;
if (this.state.loginAttempted) {
if (this.state.isLoggedIn) {
loginLabel = <label>Login successful</label>
} else {
loginLabel = <label>Login failed</label>
}
}
return (
<div>
<form data-test="login-form" onSubmit={e => this.handleSubmit(e)}>
<input data-test="login-input" name="login" value={this.state.userName} onChange={e => this.setUserName(e)} /><br />
<input data-test="password-input" name="password" value={this.state.password} onChange={e => this.setPassword(e)} type="password" /><br />
<input data-test="submit-button" type="submit" value="Login" /><br />
</form>
{loginLabel}
</div>
)
}
}
It works just fine, but I also want to test it using #testing-library/react
My test looks like this:
import { fireEvent, waitForElement } from '#testing-library/react';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import { Login } from './login';
import { LoginService } from './services/LoginService';
describe('Login component tests', () => {
let container: HTMLDivElement;
const loginServiceSpy = jest.spyOn(LoginService.prototype, 'login');
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
ReactDOM.render(<Login />, container);
});
})
afterEach(() => {
document.body.removeChild(container as HTMLDivElement);
container.remove();
});
it('renders correctly status label', async () => {
loginServiceSpy.mockResolvedValueOnce(false);
const button = container.querySelectorAll('input')[2];
fireEvent.click(button);
const label = await waitForElement(() => {
container.querySelector('label');
}); // THIS query times out and test fails
expect(label).toBeInTheDocument();
});
});
How can I make this async test work? Without the async functionality, the container.querySelector('label') works.
Thanks!
Figured it out myself. I was misusing waitForElement.
The proper way to use it is:
const label = await waitForElement(() =>
container.querySelector('label')
)
Related
Hi i am new developer testing platform. I have a problem but I did not find a solution or work it with correct way. I am trying to login component test with to parameter by Inputs. Firstly I filled these are userEvent.type. After I am clicking my button. And when I was waiting my method that call by onSubmitForTest in one time , I am facing an error like fallowing image.
What is the reason of this ? How can I solve my problem ? Thanks for your helps.
My Login.tsx component:
import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import Input from "../../components/Input";
import InputPassword from "../../components/Input/InputPassword";
import ButtonLoading from "../../components/Button/ButtonLoading";
import { GetLoginInfo, ILoginRequest } from "../../store/actions/loginActions";
interface ILoginState {
emailorUsername: string;
password: string;
}
const initialState = {
emailorUsername: "",
password: "",
};
interface IProps {
onSubmitForTest: (items: any) => void
}
const Login: FC<IProps> = ({ onSubmitForTest }) => {
const { t } = useTranslation();
const [state, setstate] = useState<ILoginState>(initialState);
const onChange = (key: string, value: string | number) => {
setstate({ ...state, [key]: value });
};
const handleLogin = async () => {
const loginRequest: ILoginRequest = {
emailOrUsername: state.emailorUsername,
password: state.password,
returnUrl: "",
};
const response = await GetLoginInfo(loginRequest);
if (response.isSucceed) { } else { }
};
const renderLoginPart = () => {
return (
<div className="flex">
<Input
name="emailorUsername"
label={t("emailorUsername")}
value={state.emailorUsername}
onChange={(val: any) => onChange("emailorUsername", val)}
/>
<InputPassword
name="password"
label={t("password")}
value={state.password}
onChange={(val: any) => onChange("password", val)}
/>
<ButtonLoading
text={t("login")}
variant="contained"
onClick={() => {
if (onSubmitForTest) {
const loginRequestItemForTest = {
emailOrUsername: "testUsername",
password: "testPassword",
};
onSubmitForTest(loginRequestItemForTest)
}
handleLogin()
}}
dataTestid={"login-button-element"}
/>
</div>
);
};
return <div className="">{renderLoginPart()}</div>;
};
export default Login;
My index.test.js :
import React from 'react'
import { render, screen, waitFor } from "#testing-library/react"
import LoginPage from "../index"
import userEvent from "#testing-library/user-event"
const onSubmit = jest.fn()
beforeEach(()=>{
const { } = render(<LoginPage />)
onSubmit.mockClear()
})
test('Login form parametre olarak doğru data gönderme testi', async () => {
const eMail = screen.getByTestId('text-input-element')
const password = screen.getByTestId('password-input-element')
userEvent.type(eMail, "fillWithTestUsername")
userEvent.type(password, "fillWithTestPassword")
userEvent.click(screen.getByTestId('login-button-element'))
await waitFor(()=>{
expect(onSubmit).toHaveBeenCalledTimes(1)
})
})
beforeEach(()=>{
render(<LoginPage onSubmitForTest={onSubmit} />)
})
Please try doing this in beforeEach. If this still doesn't work you can try replacing toHaveBeenCalledTimes with toBeCalledTimes like below
await waitFor(()=>{
expect(onSubmit).toBeCalledTimes(1)
})
I would like to test (with RTL and jest) the api.post (with axios) triggered by a button in my react component bellow. It's about a login display with 2 inputs fields (email and password) and 2 buttons (enter and register). Both the enter and the register buttons called asynchronous functions.
// src > pages > Login
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { saveToken } from '../app/slices/tokenSlice';
import { saveUserData } from '../app/slices/userSlice';
import api from '../services/api';
function Login() {
const [loginState, setLoginState] = useState({
email: '',
password: '',
});
const navigate = useNavigate();
const dispatch = useDispatch();
const handleChange = ({ target: { name, value } }) => {
setLoginState({
...loginState,
[name]: value,
});
};
const enterUser = async () => {
await api.post('/login', { ...loginState })
.then((response) => {
dispatch(saveToken(response.data.token));
dispatch(saveUserData(loginState));
navigate('/home');
})
.catch((error) => {
alert(error.message);
});
};
const registerUser = async () => {
api.post('/user', loginState)
.then(() => {
alert('Usuário cadastrado com sucesso!');
})
.catch((error) => {
alert(error.message);
});
};
return (
<div>
<label htmlFor="email">
Email:
<input
id="email"
name="email"
onChange={handleChange}
placeholder="Email"
type="text"
value={loginState.email}
/>
</label>
<label htmlFor="password">
Senha:
<input
id="password"
name="password"
onChange={handleChange}
type="password"
value={loginState.password}
/>
</label>
<button
name="btnEnterUser"
type="button"
onClick={() => enterUser()}
>
Entrar
</button>
<button
name="btnRegisterUser"
type="button"
onClick={() => registerUser()}
>
Cadastrar
</button>
</div>
);
}
export default Login;
// src > services > api.js
import axios from 'axios';
const { BASE_URL } = process.env;
const api = axios.create({
baseURL: BASE_URL,
});
api.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8';
api.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
export default api;
How can i develop a test with mock data related to "registerUser" and "enterUser" functions. I have tried many options with jest.fn(), jest.spyOn() and userEvent.click(btnRegister), where btnRegister is the element which i getByRole as you can check in my test file bellow:
// src > tests > login.test.js
import React from 'react';
import '#testing-library/jest-dom';
import { screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import Login from '../pages/Login';
import renderWithRouter from './renderWithRouter';
import api from '../services/api';
describe('Componente Login', () => {
let inputEmail;
let inputPassword;
let btnRegister;
let btnEnter;
const userEmail = 'mariana#gmail.com';
const userPassword = '123456';
const tokenMock = {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjA4MWYyYzBhMWFhNzFmYjhjZjU2NjAiLCJlbWFpbCI6Im1hcmlhbmFAZ21haWwuY29tIiwiaWF0IjoxNjQ1MzIyMjU1LCJleHAiOjE2NDUzNDAyNTV9.TIgJFIzg1W0bisvJ3CfRsVCZr3kbKn13_NBN-Ah1U1w',
};
const userRegisteredResponseMock = {
user:
{
email: 'vitao#gmail.com',
id: '6211a1d3eb25fc2418dec05a',
},
};
beforeAll(() => {
renderWithRouter(<Login />);
inputEmail = screen.getByLabelText(/email/i);
inputPassword = screen.getByLabelText(/senha/i);
btnRegister = screen.getByRole('button', { name: /cadastrar/i });
btnEnter = screen.getByRole('button', { name: /entrar/i });
userEvent.type(inputEmail, userEmail);
userEvent.type(inputPassword, userPassword);
});
it('Registro de usuário com sucesso', async () => {
// ************** DOESNT WORK **********************
// api.post = jest.fn().mockImplementation(() => {
// Promise.resolve(userRegisteredResponseMock);
// });
// api.post = jest.fn(() => Promise.resolve(userRegisteredResponseMock));
// api.post = jest.fn().mockResolvedValue(userRegisteredResponseMock);
// jest.spyOn(Login, 'enterUser');
// ****************************************************
userEvent.click(btnRegister);
expect(<"REGISTER USER" FUNCTION>).toBeCalledTimes(1);
});
});
I also have tried created "mocks" folder as jest documentation has mentioned in this link: https://jestjs.io/docs/manual-mocks , but without success.
in order to force jest using the mocked module, the jest.mock() function should be called.
import api from '../services/api';
...
jest.mock('../services/api');
...
api.post.mockResolvedValue(userRegisteredResponseMock);
it's also possible to mock axios module itself. furthermore, there's a jest-mock-axios npm module designed to achieve the behaviour.
I need to test the register method along with the amplify auth signup which is written in my action file.
import history from "../utils/history.helper";
import { alertInfo } from "../utils/common.helper";
import { Auth } from "aws-amplify";
export const AuthAction = {
register,
};
function register(signUpData) {
return async (dispatch) => {
const {
username,
password,
company_name,
country_name,
designation,
} = signUpData;
try {
const signUpResponse = await Auth.signUp({
username,
password,
attributes: {
"custom:company_name": company_name,
"custom:country_name": country_name,
"custom:designation": designation,
},
});
alertInfo("success", "Please verify the mail,for successfull login");
history.push("/login");
} catch (error) {
alertInfo("error", error.message);
}
};
}]
This action file I'm calling from my signup component
import React from "react";
import { useForm } from "react-hook-form";
import { useDispatch } from "react-redux";
import { yupResolver } from "#hookform/resolvers/yup";
import content from "./content";
import { AuthInputField } from "../../common/FieldComponent";
import { AuthAction } from "../../../actions/auth.action";
import { signupSchema } from "../../common/Validation";
const SignupForm = () => {
const dispatch = useDispatch();
const { register, handleSubmit, errors } = useForm({
resolver: yupResolver(signupSchema),
});
const [buttonDisable, setButtonDisable] = React.useState(false);
function onSubmit(signUpData) {
setButtonDisable(true);
dispatch(AuthAction.register(signUpData));
}
return (
<div className="container" data-test="signUpContainer">
<h4>Welcome</h4>
<p>Please resigter to your account.</p>
<form data-testid="submit" onSubmit={handleSubmit(onSubmit)}>
{content.inputs.map((input, index) => {
return (
<AuthInputField
key={index}
name={input.name}
label={input.label}
placeholder={input.placeholder}
type={input.type}
register={register}
error={(errors && errors[input.name] && errors[input.name]) || {}}
/>
);
})}
<input
type="submit"
className="btn btn-primary button"
name="submit"
value={`Submit`}
role="submit"
disabled={buttonDisable}
/>
</form>
</div>
);
};
export default SignupForm;
I'm trying to find a way for testing the "Auth.signup" but didn't find any specific solution.
After spending so many hours, finally wrote these test cases for the above question.
import { register } from "./auth.action";
import mockData from "./__mocks__/mockData";
import { Auth } from "./__mocks__/aws-amplify";
import history from "../utils/history.helper";
jest.mock("./auth.action", () => {
return { register: jest.fn(() => mockPromise) };
});
jest.mock("aws-amplify");
describe("Signup action", () => {
beforeEach(() => {
register.mockClear();
});
test("Check register function have been called or not", async () => {
register();
expect(register).toHaveBeenCalled();
expect(register).toMatchSnapshot();
});
test("Check args passed in function are valid or not", () => {
expect(mockData.signupData).not.toBeNull();
expect(mockData.signupData).toMatchObject({
username: "Abhinav02#getnada.com",
});
expect(mockData.signupData).toHaveProperty("company_name", "Ces");
expect(mockData.signupData).toHaveProperty("country_name", "India");
expect(mockData.signupData).toHaveProperty("designation", "SD");
expect(mockData.signupData).toHaveProperty("password", "Password#123");
expect(mockData.signupData).toMatchSnapshot();
});
test("Amplify auth is called or not", () => {
Auth.signUp(mockData.signupData);
expect(Auth.signUp).toHaveBeenCalled();
expect(Auth.signUp).toMatchSnapshot();
});
test("history is pushed", () => {
const pushSpy = jest.spyOn(history, "push");
pushSpy("/login");
expect(pushSpy).toHaveBeenCalled();
expect(pushSpy.mock.calls[0][0]).toEqual("/login");
});
});
I have written the amplify auth test case in mock file.
// in __mocks__/aws-amplify.js
export const Auth = {
currentSession: jest.fn(() => Promise.resolve()),
signUp: jest.fn(() => Promise.resolve()),
signIn: jest.fn(() => Promise.resolve()),
};
Hope it helps others as well who are looking for the same.
I configured a container to test with redux-mock-store to the last version and I get some issues. The find() function not works. I ever receive zero nodes and zero length. When I use mount instead to shallow function this works but I get the issues where the redux mapDispatchToProps is not recognized. How I can guarantee that action will be called? I don't wanna test the store but the action function because I use thunk. Is my reasoning right?
My container:
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { Redirect } from 'react-router-dom'
import styles from './Auth.module.css'
import Input from '../../components/UI/Input/Input'
import Button from '../../components/UI/Button/Button'
import Logo from '../../components/UI/Logo/Logo'
import Spinner from '../../components/UI/Spinner/Spinner'
import { auth as authAction } from '../../store/actions/index'
import { checkValidity } from '../../shared/utility'
export const Auth = (props) => {
const [formIsValid, setFormIsValid] = useState(false)
const [authForm, setAuthForm] = useState({
email: {
elementType: 'input',
elementConfig: {
type: 'email',
placeholder: 'Enter your email'
},
value: '',
validation: {
required: true,
isEmail: true
},
valid: false,
touched: false
},
password: {
elementType: 'input',
elementConfig: {
type: 'password',
placeholder: 'Enter your password'
},
value: '',
validation: {
required: true,
minLength: 6
},
valid: false,
touched: false
},
})
const inputChangeHandler = (event, controlName) => {
const updatedControls = {
...authForm,
[controlName]: {
...authForm[controlName],
value: event.target.value,
valid: checkValidity(event.target.value, authForm[controlName].validation),
touched: true
}
}
let formIsValid = true;
for (let inputIdentifier in updatedControls) {
formIsValid = updatedControls[inputIdentifier].valid && formIsValid
}
setAuthForm(updatedControls)
setFormIsValid(formIsValid)
}
const submitHandler = (event, signup) => {
event.preventDefault()
props.onAuth(
authForm.email.value,
authForm.password.value,
signup
)
}
const formElementsArray = []
for (let key in authForm) {
formElementsArray.push({
id: key,
config: authForm[key]
})
}
let formFields = formElementsArray.map(formElement => (
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
changed={(event) => inputChangeHandler(event, formElement.id)} />
))
let form = (
<>
<form onSubmit={(event) => submitHandler(event, false)}>
{formFields}
<Button
disabled={!formIsValid}
btnType="Default">Log In</Button>
</form>
<Button
clicked={(event) => submitHandler(event, true)}
disabled={!formIsValid}
btnType="Link">Sign Up</Button>
</>
)
if (props.loading) {
form = <Spinner />
}
const errorMessage = props.error ? (
<div>
<p style={{ color: "red" }}>{props.error}</p>
</div>
) : null;
let authRedirect = null;
if (props.isAuthenticated) {
authRedirect = <Redirect to={'/'} />
}
return (
<main className={styles.Auth}>
{authRedirect}
<div className={styles.AuthForm}>
<h1>Log in to your account</h1>
<Logo height="3em" />
{errorMessage}
{form}
</div>
</main>
)
}
const mapStateToProps = (state) => {
return {
loading: state.auth.loading,
error: state.auth.error,
isAuthenticated: state.auth.token !== null,
}
}
const mapDispatchToProps = (dispatch) => {
return {
onAuth: (email, password, isSignup) => dispatch(authAction(email, password, isSignup))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Auth)
My test:
import React from 'react';
import { Redirect } from 'react-router-dom';
import thunk from 'redux-thunk';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import configureStore from 'redux-mock-store';
import Auth from './Auth';
import Spinner from '../../components/UI/Spinner/Spinner';
import Button from '../../components/UI/Button/Button';
import Input from '../../components/UI/Input/Input';
configure({ adapter: new Adapter() });
const setup = () => {
const props = {
onAuth: jest.fn()
}
const middlewares = [thunk]
const mockStore = configureStore(middlewares);
const initialState = {
auth: {
token: null,
email: null,
error: null,
loading: false
}
};
const store = mockStore(initialState);
const enzymeWrapper = shallow(<Auth store={store} {...props} />).dive();
return {
enzymeWrapper,
props,
store
}
}
describe('<Auth />', () => {
it('should calls onSubmit prop function when form is submitted', () => {
const { enzymeWrapper: wrapper, props: reduxProps, store } = setup();
const form = wrapper.find('form');
form.simulate('submit', {
preventDefault: () => { }
});
expect(wrapper.props().onAuth).toHaveBeenCalled();
});
});
To be able to test the Auth class without the connection to store, you need to use the named import and not the default import. PFB the line to add in your test file for importing the Auth component:
import { Auth } from './Auth'; // notice the curly braces around the component name
Also, with this approach, you need not pass store as props to the component while rendering, and you can pass the actions as mocked functions (which you are already doing for onAuth action). Also you can use shallow with this approach.
I'm sorry I'm sure this question has been answered before I did a few searches and couldn't find it.
I just am trying to figure out a way to submit a form , and get a "Successfully Submitted your information" response to the user, and clear the form.
I made a small form to make it simple - any help would be greatly appreciated.
**Edit - At the moment you click submit and everything submits into the MongoDB correctly but nothing happens on the Client side. Would like something to show that the data has indeed been added
form.js Component
import React, { Component } from 'react';
import {
Button,
Form,
FormGroup,
Label,
Input
} from 'reactstrap';
import { connect } from 'react-redux';
import { addItem } from '../actions/itemActions';
class Register extends Component {
state = {
name: '',
email: ''
};
onChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
onSubmit = e => {
e.preventDefault();
const newItem = {
name: this.state.name,
email: this.state.email
};
this.props.addItem(newItem);
this.setState({
name: "",
email: ""
});
};
render() {
return (
<div>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="item">Name</Label>
<Input
type="text"
name="name"
id="item"
placeholder="Full Name"
onChange={this.onChange}
/>
<Label for="email">Email</Label>
<Input
type="text"
name="email"
id="item"
placeholder="Email"
onChange={this.onChange}
/>
<Button color="dark" style={{ marginTop: '1rem' }} block>
Register
</Button>
</FormGroup>
</Form>
</div>
);
}
}
const mapStateToProps = state => ({
item: state.item
});
export default connect(
mapStateToProps,
{ addItem }
)(Register);
ItemActions.js
import axios from 'axios';
import { GET_ITEMS, ADD_ITEM, ITEMS_LOADING } from './types';
export const getItems = () => dispatch => {
dispatch(setItemsLoading());
axios.get('/api/items').then(res =>
dispatch({
type: GET_ITEMS,
payload: res.data
})
);
};
export const addItem = item => dispatch => {
axios.post('/api/items', item).then(res =>
dispatch({
type: ADD_ITEM,
payload: res.data
})
);
};
/* If I wanted to include a delete item request
export const deleteItem = id => dispatch => {
axios.delete(`/api/items/${id}`).then(res =>
dispatch({
type: DELETE_ITEM,
payload: id
})
);
};*/
export const setItemsLoading = () => {
return {
type: ITEMS_LOADING
};
};
items.js
const express = require('express');
const router = express.Router();
// Item Model
const Item = require('../../models/Items');
// route GET api/items
router.get('/', (req, res) => {
Item.find()
.sort({ name: 1 })
.then(items => res.json(items));
});
// route POST api/items
router.post('/', (req, res) => {
const newItem = new Item({
name: req.body.name,
email: req.body.email
});
newItem.save();
});
module.exports = router;