I need to explain behavior of react-testing-library.
I'm trying to test my login page which works fine in browser but I am not able to pass any test.
Currently I am facing two problems:
this.props.anyFunction which is passed from test throws not a function
component is not being re-rendered on state change
Login.test.js:
let __login = render(<Login onLogin={()=>console.log('Im in!')}/>);
// fill some inputs
fireEvent.click(submit);
If user is logged in
I want just to console.log this situation at this moment but Login component throws error this.props.onLogin is not a function even if it is passed.
If user is NOT logged in
In this situation I render message in Login.js:
<div role="alert" className="text-danger px-2 mb-1">
{_(`login.error.${this.state.error}`)}
</div>
which works fine in browser. So I've tried to catch it in Login.test.js:
const alert = await __login.findByRole('alert');
which throws error Unable to find an element with the role "alert". In dump I see whole rendered login page but nothing has changed (component did not re-render after unsucessful try).
Is there anything what I didn't understand well?
Is there any way how to debug state of tested component?
Full code example
As requested I'm providing full code example. May the router or socket.io cause break?
Login.js
class Login extends React.Component
{
constructor(props) {
super(props);
this.state = {email:'', password:'', error:false}
}
componentDidMount() {
// I know this is not best way how to handle authorization, but it's just for now
this.props.io.on('onTokenResponse',(response)=> {
if (response.error) {
this.setState({error:response.error});
return;
}
if (!response.token) {
this.setState({error:'unknown error'});
return;
}
sessionStorage.setItem('token', JSON.stringify(response));
this.props.onLogin();
this.props.history.push('/');
});
}
handleSubmit=(e)=>{
e.preventDefault();
this.props.io.emit('onTokenRequest',{email:this.state.email, password:this.state.password});
};
handleChange=(e)=>
this.setState({[e.target.name]:e.target.value});
Message=()=>
this.state.error && <div role="alert" className="text-danger px-2 mb-1">{_(`login.error.${this.state.error}`)}</div>;
render() {
return (
<section className="login-box mt-5">
{this.Message()}
<form onSubmit={this.handleSubmit}>
{['email','password'].map((e)=>
<input key={e} name={e} type={e} placeholder={e} required="required"
className="form-control mb-2"
value={this.state[e]}
onChange={this.handleChange}/>
)}
<button type="submit" className="btn btn-primary text-capitalize w-100">
{_('login.submit')}
</button>
</form>
</section>
);
}
}
export default withRouter(Login);
Login.test.js
test('renders without crashing', async () =>
{
const ioClient = io.connect(config.webSocketServer);
const onLogin = jest.fn();
let __login = render(
<BrowserRouter>
<Switch>
<Login io={ioClient} onLogin={onLogin}/>
</Switch>
</BrowserRouter>
);
fillAndCheckInput('email','jaxpcze#gmail.com');
fillAndCheckInput('password','wrong');
let submit = __login.getByText('přihlásit');
fireEvent.click(submit);
const alert = await __login.findByRole('alert');
fillAndCheckInput('password','right');
fireEvent.click(submit);
await expect(onLogin).toHaveBeenCalledTimes(1);
function fillAndCheckInput(placeholder,value) {
let __input = __login.getByPlaceholderText(placeholder);
fireEvent.change(__input,{target:{value:value}});
expect(__input.value).toBe(value);
}
});
Related
I've made a modal for a simple log in page for a website:
import React from 'react';
import { withRouter } from 'react-router-dom';
import '../../assets/stylesheets/session/login_form.css';
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: {}
};
this.handleSubmit = this.handleSubmit.bind(this);
this.renderErrors = this.renderErrors.bind(this);
this.handleDemo = this.handleDemo.bind(this);
}
// After authentication redirect user to home page
componentWillReceiveProps(nextProps) {
if (nextProps.currentUser === true) {
this.props.history.push('/');
this.props.closeModal();
}
// Setting or clearing errors
this.setState({ errors: nextProps.errors });
}
// Hides scrolling when modal is mounted
componentDidMount() {
if (this.props.modal) document.body.style.overflow = 'hidden';
}
// Reactiviates scrolling when modal is unmounted
componentWillUnmount() {
document.body.style.overflow = 'unset';
}
// Render the session errors if there are any
renderErrors() {
return (
<ul>
{Object.keys(this.state.errors).map((error, i) => (
<li key={`error-${i}`}>{this.state.errors[error]}</li>
))}
</ul>
);
}
// Handle field updates
update(field) {
return e =>
this.setState({
[field]: e.currentTarget.value
});
}
// Handle form submission
handleSubmit(e) {
e.preventDefault();
let user = {
email: this.state.email,
password: this.state.password
};
if (this.props.errors) {
this.props.login(user)
.then(() => this.props.openModal('login'));
} else {
this.props.login(user)
.then(() => this.props.closeModal());
}
}
// Handle demo user login
handleDemo(e) {
e.preventDefault();
const user = { email: 'demouser#nookbnb.com', password: 'password' };
this.props.login(user)
.then(this.props.history.push('/'), this.props.closeModal());
}
// Rendering component
render() {
let errors;
if (this.props.errors) {
errors = this.props.errors;
} else {
errors = {};
}
let emailErrors = errors.email ? <div className="email-error">{errors.email}</div> : '';
let passwordErrors = errors.password ? <div className="password-error">{errors.password}</div> : '';
return (
<div className="login-modal-wrapper">
<div className="modal-wrapper" onClick={this.props.closeModal}></div>
<form onSubmit={this.handleSubmit}>
<div className="header-wrapper">
<div className="close-wrapper" onClick={this.props.closeModal}>
<i className="close-button"></i>
</div>
<h1>Log in</h1>
</div>
<div className="main-content-wrapper">
<button onClick={this.handleDemo}>
Demo Log in
</button>
<div className="button-separator-wrapper"><p>or</p></div>
<input
type="text"
value={this.state.email}
onChange={this.update('email')}
placeholder="Email"
/>
<input
type="password"
value={this.state.password}
onChange={this.update("password")}
placeholder="Password"
/>
<div className="session-errors">
{emailErrors}
{passwordErrors}
</div>
<button type="submit">Log in</button>
<div className="no-account-wrapper">
<p>Don't have an account? <span onClick={() => this.props.openModal('signupFirst')}>Sign up</span></p>
</div>
</div>
</form>
</div>
);
}
}
export default withRouter(LoginForm);
And I've successfully displayed the right error messages when the user doesn't enter a required field in the login form (an email and a password), but if I don't manually do a page refresh, the errors still appear on the form even after I close and reopen the modal.
How can I implement this modal in a way where it will automatically clear errors after I close and reopen the modal?
UPDATE
Per the answer below, I've added these two open and closing modal functions to help clear the errors:
// Opens a login modal
openLoginModal() {
this.setState({ errors: {} });
this.props.openModal('login');
}
// Closes a login modal
closeLoginModal() {
this.setState({ errors: {} });
this.props.closeModal();
}
And I've replaced places in the render where I'm using this.props.closeModal() and this.props.openModal() with my functions above. (For now I'm just testing this with closing the modal; since the modal doesn't have any errors when initially opened, I believe I just need to account for closing the modal right now)
<div className="login-modal-wrapper">
<div className="modal-wrapper" onClick={this.closeLoginModal}></div>
<form onSubmit={this.handleSubmit} className={errors.email && errors.email.length !== 0 ? 'form-errors' : 'form-normal'}>
<div className="header-wrapper">
<div className="close-wrapper" onClick={this.closeLoginModal}>
<i className="close-button"></i>
</div>
...
But error messages are still persisting when I open and close the modal.
Perhaps consider having an openLoginModal method that clears any errors and then opens the modal:
openLoginModal() {
this.setState({ errors: {} })
this.props.openModal('login');
}
And then replace any occurrence of this.props.openModal('login') to use this new method (this.openLoginModal()).
Edit: If you need to clear the errors specifically on exiting the modal, you can do a similar thing by creating a custom method around the closeModal prop.
I have components in my react single page web app that require a password to view. Upon clicking the 's, a password form component is rendered. I have the logic written to check if the password is correct. If it is correct, how do I then leave the password form component, and render the component that the link was originally headed to?
I've tried just toggling visibility but I think I'm mostly confused on how specifically to use React Router to render a component on the condition that the password was correct
Parent Component
handleClick = (e) => {
e.preventDefault();
this.setState({ isPasswordVisible: !this.state.isPasswordVisible });
}
render() {
return (
<div className="BigNames">
<Link onClick={this.handleClick} className="BigNames-link" to='/Adobe' style={{textDecoration:'none'}}>
<span className='Name'>Adobe Creative Cloud</span>
<span className='Text'>: App Banner</span> <i className="fas fa-lock"></i>
</Link>
Password Component
import React, { Component } from 'react';
import './Password.css';
import Adobe from './Big Work/Adobe';
export default class Password extends Component {
static defaultProps = {
password: 'pierpoint'
}
constructor(props) {
super(props)
this.state = {
visible: true,
value: ''
}
this.handleClick = this.handleClick.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(e) {
e.preventDefault();
this.setState({value: e.target.value});
}
handleSubmit(e) {
e.preventDefault();
if(this.state.value === this.props.password) {
alert('Correct!');
this.setState({visible: false});
return( <Adobe />)
} else (alert('Incorrect Password!'))
}
handleClick(event) {
event.preventDefault()
this.setState(prevState => ({
visible: !prevState.visible,
}))
}
render() {
if (!this.state.visible) {
return null
}
return (
<div className="pwd">
<div className="enter-pwd">
<button className='exit' onClick={this.handleClick}> ✕ </button>
<form onSubmit={this.handleSubmit}>
<input
className="sub-text"
type='password'
name='password'
placeholder='Enter password'
value={this.state.value}
onChange={this.handleChange}>
</input>
<button
className="sub-mit"
type='submit'>
submit
</button>
</form>
</div>
</div>
)
}
}
The password component does go away after a correct password is submitted, however the following conditional component doesn't render.s
Here is the codepen showing the full example : Hidden by password page
In my example, the hidden page is a component I called SecretPage and the form handling the password is called Password. The parent component is App.
Because I needed to know inside of App whether the password was correct or not, the first step was to make Password a controlled component.
function Password(props){
return (
<div>
<p>Maybe the secret is a potato ?</p>
<form onSubmit={props.onSubmit}>
<input type='password' value={props.password} onChange={props.onChange}/>
<input type='submit' value='submit'/>
</form>
</div>);
}
What that means is simply that onSubmit, onChange and the value of password input itself are all given as props, and are handled by App and not by Password itself.
Here is how Password is called inside the App function
<Password password={this.state.password} onChange={this.handleChange} onSubmit={this.handleSubmit} />
Whenever the form is submitted, the function handleSubmit from App is called and it looks like this:
handleSubmit(e){
e.preventDefault();
this.setState({
secretVisible : this.checkPassword(this.state.password),
});
}
Because secretVisible is a state of the App now, knowing which page it should display is really easy. It only needs to check the this.state.secretVisible.
render(){
const secretVisible = this.state.secretVisible;
let pageToDisplay;
if(secretVisible){
pageToDisplay = <SecretPage onTakeMeBackClicked={this.handleLogOff}/>;
}
else{
pageToDisplay = <Password password={this.state.password} onChange={this.handleChange} onSubmit={this.handleSubmit} />;
}
return (
<div>
{pageToDisplay}
</div>
);
}
}
There are several ways to handle it. You can F.E use protected route togheter with react-router or in a simple case you can:
class App extends Component {
state = {
isAuthenticated: false,
}
setIsAuthenticated = (bool) => {
this.setState({isAuthenticated: bool})
}
render(){
const { isAuthenticated } = this.state;
return(
if(isAuthenticated){
return <YourMainComponent />
)
return <AuthFormComponent setIsAuthenticated={this.setIsAuthenticated} />
}
}
This is just an example, but I hope it gives you a tip on how to handle it.
There's not a ton of code here to give a proper example, but in pseudocode you'll want to do something like:
<div>
{isPasswordVerified
? <ComponentYouWantToShow />
: <Password callbackProp={setIsPasswordVerified} />
}
</div>
The Password component needs a callback prop to send whether the verification was successful to the parent component. Then in the parent component you can conditionally render the appropriate component. No need to deal w/ Routing here.
I'm creating a project-planning app using React, Redux, and Firebase. A single project record in my Firestore database contains a Title and some Content. When I go to update a project, I have the input fields' defaultValues set to the correct data for the project I want to edit. However, updating only works if I make changes to both the Content and Title input fields. Otherwise, upon submitting these values the data gets deleted because the local state has not seen any changes and therefore updates the untouched field to the empty string: ""
I have tried setting the local state of the EditProject component in the render method, but this is not possible:
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
this.setState({
title: project.title,
content: project.content
});
...
I have also tried setting the state in during componentDidMount like so:
componentDidMount = () =>{
const { project } = this.props;
this.setState({
title: project.title,
content: project.content
})
}
But the issue with this is that the project prop does not get mapped by mapStateToProps before componentDidMount
Lastly, I've tried passing the project prop from the parent component, which is projectDetails, but I am unable to successfully do so. I might be doing this part wrong so please let me know if there is a good way to do this with the code I have. In ProjectDetails:
<Link to={"/edit/" + docId} key={docId}>
<button className="btn pink lighten-1 z-depth-0">Edit</button>
</Link>
This links to the 'broken' EditDetails component I am trying to fix.
Here is my code for the EditProject component
class EditProject extends Component {
state = {
title: "",
content: ""
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
let localProject = this.state;
let docId = this.props.docId;
this.props.editProject(localProject, docId);
const projectDetailURL = "/project/" + docId;
this.props.history.push(projectDetailURL);
};
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
<div className="card-content">
<form onSubmit={this.handleSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Project</h5>
<div className="input-field">
<label htmlFor="title" className="active">
Title
</label>
<input
onChange={this.handleChange}
type="text"
id="title"
defaultValue={project.title}
/>
</div>
<div className="input-field">
<label htmlFor="content" className="active">
Edit Project Content
</label>
<textarea
id="content"
onChange={this.handleChange}
className="materialize-textarea"
defaultValue={project.content}
/>
</div>
<div className="input-field">
<button className="btn pink lighten-1 z-depth-0">
Update
</button>
</div>
</form>
</div>
<div className="card-action grey lighten-4 grey-text">
<div>
Posted by {project.authorFirstName} {project.authorLastName}
</div>
<div>{moment(project.createdAt.toDate()).calendar()}</div>
<div className="right-align" />
</div>
</div>
</div>
);
} else {
return (
<div className="container center">
<p>Loading project...</p>
</div>
);
}
}
}
const mapStateToProps = (state, ownProps) => {
//id = the document id of the project
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null;
return {
project: project,
auth: state.firebase.auth,
docId: id
};
};
const mapDispatchToProps = dispatch => {
return {
editProject: (project, docId) => dispatch(editProject(project, docId))
};
};
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
firestoreConnect([
{
collection: "projects"
}
])
)(EditProject);
Upon visiting the edit page, I would like the data to remain unchanged if a user does not make any changes to an input field.
I was able to properly update my local state by using React Router to pass props to my EditProject component from its "parent component". I used the React router to do this since the EditProject component is not actually nested inside this "parent component".
Here's how you can pass props to other components using React Router:
Specify where you want to send your props and what you want to send:
//ProjectDetails Component
<Link to={{
pathname: "/edit/" + docId,
state: {
title: project.title,
content: project.content
}
}}>
<button className="btn">Edit</button>
</Link>
Aquire props in the componentDidMount() lifecycle method and update the local state using setState().
//EditProject Component (component recieving props from ProjectDetails)
class EditProject extends Component {
state = {
title: "",
content: ""
};
componentDidMount = () => {
//Aquire proprs from React Router
const title = this.props.location.state.title
const content = this.props.location.state.content
//Update the local state
this.setState({
title: title,
content: content
})
}
I hope this helps!
I have a page where a user can search a database for a given condition, then the data is returned with another button that the user can use to add information back to the database. However whenever I click on the second button, the page reloads. I can't get so much as a console.log to go in. I'm new to react and could use any help at all.
import React , { Component } from 'react';
import { database } from '../firebase';
const byPropKey = (propertyName, value) => () => ({
[propertyName]: value,
});
class Search extends Component{
constructor(props) {
super(props);
this.state={
users: null,
searchCondition: "",
friend: ""
}
// this.setState = this.setState.bind(this);
}
onSubmit = (event) => {
let {
searchCondition,
friend
} = this.state;
database.searchConditions(searchCondition).then(snapshot =>
this.setState(() => ({ users: snapshot.val() }))
);
event.preventDefault();
}
messageSubmit = (event) => {
console.log("Click")
}
render(){
let {
users,
searchCondition,
friend
} = this.state;
return(
<div>
<h1>Search for conditions</h1>
<form onSubmit={this.onSubmit}>
<div className="search">
<input
value={searchCondition}
onChange={event => this.setState(byPropKey('searchCondition', event.target.value))}
type="text"
placeholder="Condition to Search For"
/>
<button className="friendButton"
onClick="x"
type="submit">
Search
</button>
</div>
</form>
{!!users && <UserList users={users} />}
</div>
)
}
}
let UserList = ({ users, message }) =>
<div>
<h2>List of Usernames and Conditions of your Search</h2>
{Object.keys(users).map(key =>
<div key={key}>{users[key].username} : {users[key].condition}
<form>
<div className="search">
<input
value={message}
onChange={console.log("test")}
type="text"
placeholder="Message for this User"
/>
<button className="messageButton"
onClick={console.log(message)}
type="submit">
Message
</button>
</div>
</form>
</div>
)}
</div>
export default Search;
Have you tried to place the event.preventDefault() at the beginning of the event handler?
It should prevent the default behaviour imediately as the event gets fired.
Hope it works!
a couple things i can see, youre even.preventDefault() should be at the top of the page, you said it was reloading so thats unwanted behavior. second you should set state within the then, generally speaking in my experience that doesnt work- i believe due to setState being asynchronous or something of that nature.
i would rewrite your submit like this
onSubmit = (event) => {
event.preventDefault();
let {
searchCondition,
friend
} = this.state;
let value;
database.searchConditions(searchCondition).then(snapshot =>
value = snapshot.val
);
this.setState(() => ({ users: value) }))
}
also likely the reason your "messageSubmit()" was not console logging is because youre using a submit handler not a click handler so everytime your clicked you were reloading the page.
cheers
When I am submiting the form submitSucceeded props goes true, pristine is also working fine but submitting props not change on submit form. I have attached related code. Please suggest me how I can fix this issue.
import React from 'react'
import { Field, reduxForm } from 'redux-form'
import FileInput from '../FileInput'
import 'react-widgets/dist/css/react-widgets.css';
import './reactForm.css';
const EditForm = (props) => {
const { handleSubmit, submitSucceeded, pristine, submitting, owners, cities, compound, avatarUrl, changeAvatar } = props;
return (
<form onSubmit={handleSubmit}>
<div className="row padding-20-0">
<div className="col-md-4">
<div className="box-upfile cursor" style={{backgroundImage: `url(${avatarUrl})`}} >
<div className="editImgComp" >
<i className="sprite-icon icon-030" onClick={()=>{changeAvatar(null); props.change('avatar', null)}}/>
<label html="imageBrowse">
<FileInput
onDone={(file)=> {changeAvatar(file.file); props.change("avatar", file.file)}}
type="file" className="hidden" id="imageBrowse"/>
<i className="sprite-icon icon-031"/>
</label>
</div>
</div>
</div>
</div>
<div className="row">
<div className="text-right col-xs-6">
{
submitSucceeded ?
<button type="button" className="btn ls-btn-red cursor" disabled={pristine || submitting || submitSucceeded}>
<i className='fa fa-circle-o-notch fa-spin'></i> Saving
</button>
:
<button type="submit" className="btn ls-btn-red cursor" disabled={pristine || submitting} onClick={handleSubmit} >Save</button>
}
</div>
</div>
</form>
)
}
export default reduxForm({
form: 'compoundForm' // a unique identifier for this form
})(EditForm)
Container:-
handleSubmit(data) {
this.props.dispatch(compoundSave(data));
}
Action:-
export function compoundSave(data) {
const id = data.id;
const config = {
method: 'put',
body: JSON.stringify({compound: data}),
};
return callApi('/v1/compounds/'+id, {}, config, compoundSaveRequest, compoundSaveSuccess, compoundSaveFailure);
}
Call Api method:-
`export function callApi(path, params, config, request, onRequestSuccess, onRequestFailure) {
const API_ROOT = 'http://api.dev.leasing.clicksandbox.com:8080';
const idToken = localStorage.getItem('id_token');
let url = API_ROOT+path;
url = buildUrlWithQueryString(url, params);
return dispatch => {
dispatch(request);
return fetch(url, config)
.then(checkStatus)
.then(parseJSON)
.then((json) => {
if (!json.success) { // (response.status < 200 || response.status > 300)
json.error &&
Toastr.error(json.error);
dispatch(onRequestFailure(json));
} else {
json.message &&
Toastr.success(json.message);
dispatch(onRequestSuccess(json));
}
}).catch((error) => {
const exceptionMessage = {
success: false,
error: "Something went wrong!"
}
dispatch(onRequestFailure(exceptionMessage));
});
};
}`
Please let me know if I need to explain more.
For any one else arriving here a year later like me looking for answers, and without seeing the Container code... i can infer that the problem is that you defined the method handleSubmit in the Container and send it as a prop to EditForm. The problem with that is that a Component that has the reduxForm() HOC applied, in this case EditForm, will generate its own handleSubmit prop, and therefore a conflict arises, leading to wrong behaviour of the form when submitting.
In order to fix the issue, you should have send the prop to EditForm with a different name say
<EditForm onSubmit={this.onSubmit} />
And then inside the EditForm component use it like:
...
<form onSubmit={handleSubmit(onSubmit)}>
That way the prop submitting of the reduxForm component will work if the submit handler returns a promise.
you should call handleSubmit() method with handler passed to component :
<form onSubmit={handleSubmit(this.props.onSubmit)}>