Test react-final-form submit - jestjs

I have started migrating from redux-form to react-final-form to make my bundle smaller. I had couple of tests for my forms and one of them was to test that the correct action was called on form submit. Store action in my test never gets called after switching to react-final-form.
Is there a way ho to test submit function when form is passed as a property.
My test:
it('submits the form', () => {
const wrapper = shallowUntilTarget(<LoginFormContainer store={store} />, 'form');
wrapper.find('form').simulate('submit');
expect(store.getActions()).toEqual(expect.arrayContaining([{ formObj: {}, type: 'PATIENT_LOGIN_REQUEST' }]));
});
shallowUntilTarget renders the actual form through container
Tested component:
class LoginForm extends React.Component<Props> {
submitForm = (values) => {
this.props.dispatch(actions.loginPatient(values));
};
render() {
return (
<Form
onSubmit={this.submitForm}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} />

I was not able to test the validation with redux-form, but actually it is much easier to do in react-final-form. The form doesn't not get submitted and fails when validation is not successful. My LoginForm has email and password validation
<Form
onSubmit={this.submitForm}
validate={createValidator({
email: [required, email],
password: [required, minLength('8')],
})}
render={({ handleSubmit }) => (
There could be two tests. One testing failure and one testing successful submit. Both of them have to happened on mounted component.
it('does not submits invalid form ', () => {
const wrapper = mount(<LoginFormContainer store={store} />);
wrapper.find('form').simulate('submit');
expect(store.getState().lastAction).not.toEqual({ payload: {}, type: 'PATIENT_LOGIN_REQUEST' });
});
it('submits valid form', () => {
const wrapper = mount(<LoginFormContainer store={store} />);
const email = wrapper.find('input[name="email"]');
email.instance().value = 'cerny#seznam.cz';
email.simulate('change', email);
const password = wrapper.find('input[name="password"]');
password.instance().value = '12345678';
password.simulate('change', password);
wrapper.find('form').simulate('submit');
expect(store.getState().lastAction).toEqual({
payload: { email: 'cerny#seznam.cz', password: '12345678' },
type: 'PATIENT_LOGIN_REQUEST',
});
});

Related

Testing a React component that uses redux toolkit and RTKQuery

I have been making an app using redux toolkit and RTKQuery, and hit a stumbling block on how to test a component that uses slices:
Component
export const Status = () => {
const selectedKidId = useSelector(getSelectedKidId);
const { selectedKid } = useGetKidsQuery(undefined, {
selectFromResult: ({ data }) => ({
selectedKid: data?.find((kid: KidType) => kid.id === selectedKidId),
}),
});
return (
<section>
<p>
Active:{' '}
{selectedKidId !== null ? selectedKid?.firstName : 'Select a kid'}
</p>
</section>
);
};
Test
test('title renders as expected', () => {
renderWithProviders(<Status />, {
preloadedState: { kids: { selectedKidId: '0' } },
});
expect(screen.getByText(/Monsters!/i)).toBeInTheDocument();
});
As you see I can add a selectedKidId in the preloadedState but the component also uses a generated hook useGetKidsQuery which return a list of kids, I don't know how or if I can add this to preloadedState as its an apiSlice.
How would I get my list of kids data into this test?

Testing and mocking fetch in async useEffect and async Redux-Saga

I'm testing a functional component, that use React-Hooks and Redux-Saga. I can pass parameters in URL for the component, because they are a login page component.
My URL i pass is 'localhost/access/parameter', and when this parameter exists, i need to call a async redux saga, and if the fetch is OK, i put the result in redux-store. When the result is on redux-store, i have a useEffect that verify the result and if is OK, i put her in a input.
I can mock the result with axios, but i'm migrating to use only fetch. i mock the fetch, but when i use
mount(component), provided by enzyme, i do not how to await the redux-saga termine the request and useEffect do your job. I put a console log inside a effect, saga and log the input props to see your value prop, but the value is always empty . I tried to use setImmediate() and process.nextTick().
Links i use to write the code: 1,2, 3
I'm using formik, so they pass some props to me.
My component
const Login = ({
setFieldError, errors, response, fetchDomain, location, values, handleChange, handleBlur, setFieldValue, history,
}) => {
useEffect(() => {
async function fetchUrlDomain() {
const { pathname } = location;
const [, , domain] = pathname.split('/');
if (typeof domain !== 'undefined') {
await fetchDomain(domain);
}
}
fetchUrlDomain();
}, [fetchDomain, location]);
useEffect(() => {
if (typeof response === 'string') {
setFieldError('domain', 'Domain not found');
inputDomain.current.focus();
} else if (Object.keys(response).length > 0) {
setFieldValue('domain', response.Domain);
setFieldError('domain', '');
}
}, [response, setFieldValue, setFieldError]);
return (
<input name="domain" id="domain" value={values.domain} onChange={handleChange} onBlur={handleBlur} type="text" />
);
}
const LoginFormik = withFormik({
mapPropsToValues: () => ({ domain: '' }),
enableReinitialize: false,
validateOnBlur: false,
validateOnChange: false,
})(Login);
const mapStateToProps = () => ({ });
const mapDispatchToProps = dispatch => ({
fetchDomain: (value) => {
dispatch(action({}, constants.RESET_RESPONSE_DOMAIN));
dispatch(action(value, constants.FETCH_DOMAIN_REQUEST));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginFormik);
My Saga
export function* fetchDomain(action) {
const url = yield `${mainUrl}/${action.payload}`;
try {
const response = yield fetch(url).then(res => res.json());
yield put(reduxAction(response , constants.FETCH_DOMAIN_SUCCESS));
} catch (e) {
yield put(reduxAction(e, constants.FETCH_DOMAIN_FAILURE));
}
}
My Reducer
case constants.FETCH_DOMAIN_FAILURE:
return { ...initialState, response: 'Domain not found' };
case constants.FETCH_DOMAIN_SUCCESS: {
const { payload } = action;
return {
...initialState,
id: payload.Id,
apis: payload.Apis,
response: payload,
};
}
case constants.RESET_RESPONSE_DOMAIN:
return { ...initialState };
My Test
it('input with fetch only', (done) => {
const mockSuccessResponse = {
Id: 'fafafafa',
Apis: [],
Domain: 'NAME',
};
const mockJsonPromise = Promise.resolve(mockSuccessResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const wrapper = mount(
<Provider store={store}>
<LoginForm
history={{ push: jest.fn() }}
location={{ pathname: 'localhost/login/Domain' }}
/>
</Provider>,
);
process.nextTick(() => {
const input = wrapper.find('#domain');
console.log(input.props());
expect(input.props().value.toLowerCase()).toBe('name');
global.fetch.mockClear();
done();
});
});
I expect my input have value, but he don't. I tried to use jest-fetch-mock but just don't work, and i want to use native jest methods, no thirty party libraries.
I cannot say what's wrong with your current code. But want to propose different approach instead.
Currently you are testing both redux part and component's one. It contradicts with unit testing strategy when ideally you should mock everything except module under the test.
So I mean if you focus on testing component itself it'd be way easier(less mocks to create) and more readable. For that you need additionally export unwrapped component(Login in your case). Then you can test only its props-vs-render result:
it('calls fetchDomain() with domain part of location', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
shallow(<Login fetchDomain={fetchDomain} location={location} />);
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('example.com');
});
it('re-calls fetchDomain() on each change of location prop', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
const wrapper = shallow(<Login fetchDomain={fetchDomain} location={location} />);
fetchDomain.mockClear();
wrapper.setProps({ location: { pathName: 'another.org/path' } });
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('another.org');
});
And the same for other cases. See with this approach if you replace redux with direct call to fetch() or whatever, or if you refactor that data to come from parent instead of reading from redux store you will not need to rewrite tests removing mocks to redux. Sure, you will still need to test redux part but it also can be done in isolation.
PS and there is no profit to await fetchDomain(...) in useEffect since you don't use what it returns. await does not work like a pause and that code may rather confuse reader.

Cannot redirect to new page on first submission of form with history.push()

Edit
I've done some more debugging and here is the problem:
CreateProfile.js calls profileActions.createProfile() and passes data to be operated on and this.props.history so that it can push a new path onto the history stack.
profileActions.createProfile() successfully sends data to database. Database successfully uses the data.
profileActions.createProfile() pushes new path onto stack. The component at the path loads and successfully calls a reducer.
The URL in the browser does not reflect the path that is pushed onto the history stack. The new component does not load.
This only happens when creating an entry in the database. When updating an entry, the program works as expected.
I'm currently trying to redirect to a new page with react/redux. On the first submission, the form submits to the backend and creates an entry in the database but fails to redirect to the next page. On the second submission, however, it redirects just fine.
I'm using this.props.history.push() to do the redirect.
I think It may be an issue with the the response received from the backend but I cannot seem to figure out what the issue is. The reason I believe this is because it is hitting different logic because on the second submission, it is updating and not creating an entry.
Here is my component (CreateProfile.js)
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { createProfile } from '../../actions/profileActions';
import TextAreaGroup from '../common/TextAreaGroup';
import InputGroup from '../common/InputGroup';
class CreateProfile extends Component {
// Constructor
// componentWillRecieveProps()
onSubmit = (evt) => {
evt.preventDefault();
const profileData = {
handle: this.state.handle,
bio: this.state.bio,
website: this.state.website,
twitter: this.state.twitter,
instagram: this.state.instagram,
youtube: this.state.youtube,
linkedin: this.state.linkedin,
github: this.state.github,
vsco: this.state.vsco
};
this.props.createProfile(profileData, this.props.history);
}
//onChange()
render() {
// render logic
return (
// markup
<form onSubmit={this.onSubmit}>
// markup
<input
type="submit"
value="Create Profile"
className="btn btn-info btn-block mt-4"
/>
</form>
</div>
</div>
</div>
</div>
)
}
}
CreateProfile.propTypes = {
createProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
profile: state.profile,
errors: state.errors
});
export default connect(mapStateToProps, { createProfile })(withRouter(CreateProfile));
Here is my action file that submits to the backend (profileActions.js):
import axios from 'axios';
// import types
import { GET_PROFILE, PROFILE_LOADING, GET_ERRORS, CLEAR_CURRENT_PROFILE } from './types';
// Create Profile
export const createProfile = (profileData, history) => dispatch => {
axios.post('/api/profile', profileData)
.then(res => history.push('/login'))
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
})
};
}
And here is the route in my backend that is being submitted to:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validateProfileInputs(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const profileFields = {}; //code setting fields omitted
Profile.findOne({user: req.user.id}).then(profile => {
if (profile) {
// Update Profile
Profile.findOneAndUpdate(
{ user: req.user.id },
{ $set: profileFields },
{ new: true }
).then(profile => res.json(profile)); // SUCCESSFUL PUSH ONTO THIS.PROPS.HISTORY
} else {
// Create Profile
// Check if handle exists
Profile.findOne({ handle: profileFields.handle })
.then(profile => {
if (profile) {
errors.handle = 'That handle already exists';
res.status(400).json(errors);
}
new Profile(profileFields).save().then(profile => res.json(profile)); // PUSH ONTO THIS.PROPS.HISTORY NOT OCCURRING
});
}
});
});
Any and all help would be greatly appreciated. I have tried my hardest but cannot seem to figure out what the issue is.
This problem arose because of my lack of understanding of how asynchronous javascript works.
The issue was with a few lines of code in the component that I was trying to push too.
componentDidMount() {
this.props.getProfile(); // Async function, sets profile object in store
}
render() {
const { profile } = this.state.profile;
if(!Object.keys(profile).length > 0) { // This is always evaluates to true
// because it executes before
// this.props.getProfile() returns
this.props.history.push('/create-profile');
}
}

fetch not returning data in react

I'm new to react, i'm having difficulty getting data for a single book out of list, be passed through via axios' get method.
I think it has something to do with the url, but I have been unable to get fix it.
Here's my code:
export function loadBook(book){
return dispatch => {
return axios.get('http://localhost:3000/api/books/book/:id').then(book => {
dispatch(loadBookSuccess(book.data));
console.log('through!');
}).catch(error => {
console.log('error');
});
};
}
//also tried this
export function loadBook(id){
return dispatch => {
return axios.get('http://localhost:3000/api/books/book/' + {id}).then(book => {
dispatch(loadBookSuccess(book.data));
console.log('through!');
}).catch(error => {
console.log('error');
});
};
}
Html code that contains a variable link to each individual book
<div className="container">
<h3><Link to={'/book/' + book._id}> {book.title}</Link></h3>
<h5>Author: {book.author.first_name + ' ' + book.author.family_name}</h5>
<h4>Summary: {book.summary}</h4>
<BookGenre genre={genre} />
</div>
link in Route:
<Route path="/book/:id" component={BookPage} />
Edit: code for the book component
class BookPage extends React.Component{
render(){
const book = this.props;
const genre = book.genre;
console.log(book);
return(
<div>
<div>
<h3> {book.title}</h3>
<h5>Author: {book.author.first_name + ' ' + book.author.family_name}</h5>
<h4>Summary: {book.summary}</h4>
<BookGenre genre={genre} />
</div>
</div>
);
}
}
BookPage.propTypes = {
book: PropTypes.object.isRequired
};
//setting the book with mapStateToProps
function mapStateToProps (state, ownProps){
let book = {title: '', author: '', summary: '', isbn: '', genre: []};
const bookid = ownProps.params._id;
if(state.books.length > 0){
book = Object.assign({}, state.books.find(book => book.id));
}
return {
book: book
};
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(loadBook, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(BookPage);
Instead of doing this:-
axios.get('http://localhost:3000/api/books/book/' + {id})
You should do like this:-
axios.get(`http://localhost:3000/api/books/book/${id}`)
So your action.js might look like this:-
export function loadBook(id){
const request = axios.get(`http://localhost:3000/api/books/book/${id}`);
return dispatch => {
request.then(book => {
dispatch(loadBookSuccess(book.data));
}).catch(error => {
console.log('error');
})
};
}
Since the id, you have passed it seems to be a string so it can be concatenated using ES6 template strings and make sure you wrap your strings in backtick . or you can do it by + operator, also make sure you pass id as a parameter in your loadbook function so that you can join it to your URL.
Figured out the solution to this problem.
My mistake was that I failed to send the id of the item I along with the api call.
Using componentDidMount and sending the dynamic id from the url params solved this problem for me.
Thank you, #Vinit Raj, I guess I was too much of a rookie then.

Are component refs accessible in mapDispatchToProps?

I have a simple React component that has two inputs and dispatches an action to add an item to a catalog using the input values.
# components/addProduct.jsx
import React from 'react'
import { connect } from 'react-redux'
const AddProduct = ({
onClick
}) => {
let title, price
return (
<form
onSubmit= { (e) => {
e.preventDefault()
}}
>
Title: <input ref={ node => {title = node;}} type="text"/><br />
Price: <input ref={ node => {price = node;}} type="text"/><br />
<button onClick={onClick}>Create New Product</button>
</form>
)
}
function mapDispatchToProps(dispatch) {
return {
onClick: () => {
console.log("Firing on click for button")
console.log(this) # => mapToPropsProxy
console.log(AddProduct.refs) # => undefined
dispatch({ # This will be a call to addProduct(title, price) later
type: "ADD_PRODUCT",
title: this.refs.title.value, # ???
price: this.refs.price.value
})
}
}
}
export default connect(null, mapDispatchToProps)(AddProduct)
I can't access the refs I declared in my AddProduct component. This makes intuitive sense; AddProduct doesn't really even exist until connect resolves the first time with mapDispatchToProps and it gets exported.
So how can I access the input values? Am I architecting this incorrectly?
I think you are architecting this incorrectly, your function will get dispatch injected into it, so the vars you need to pass are not part of the context were it this declared, you should do something like:
<button onClick={() => {this.props.onClick(this.refs.title.value, this.refs.price.value) }}>Create New Product</button>
and the connect:
function mapDispatchToProps(dispatch) {
return {
onClick: (title, value) => {
dispatch({ # This will be a call to addProduct(title, price) later
type: "ADD_PRODUCT",
title,
price
})
}
}
}
Generally refs are only used when you need to access the DOM for some special reason. Use props and events. Something like:
<input value={title} onChange={({target:{value}}) => onTitleChanged(value)}/>
// snip
const mapDispatchToProps = dispatch => ({
onTitleChanged: newTitle => dispatch({type: 'SOME_ACTION', value: newTitle})
})

Resources