Testing Apollo React Hooks with Jest & React Testing Library - javascript

I have a form that uses an Apollo mutation hook:
import React from 'react'
import { useFormik } from 'formik'
import { useLoginMutation } from '../generated'
const LoginContainer = () => {
const [loginMutation, { data, loading, error }] = useLoginMutation()
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
onSubmit: values => {
loginMutation({
variables: {
input: {
email: String(values.email).trim(),
password: values.password,
},
},
})
},
})
return (
<form
onSubmit={event => {
event.preventDefault()
formik.handleSubmit(event)
}}
>
<input
data-testid="login-email-input"
name="email"
placeholder="Email address"
// label="Email"
required
type="email"
value={formik.values.email}
onChange={formik.handleChange}
/>
<input
data-testid="login-password-input"
name="password"
placeholder="password"
// label="Password"
required
type="password"
value={formik.values.password}
onChange={formik.handleChange}
/>
<button data-testid="login-submit-input" type="submit">
LOGIN
</button>
</form>
)
}
export default LoginContainer
I am trying to make sure the login mutation is called when the user fills in the form and clicks the submit button.
The test runs successfully sometimes and fails other times. I suspect that the loginMutation promise is not being resolved before the expect block is run.
The console also has the following warning:
Warning: An update to LoginContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
Here is the test:
describe('login container', () => {
let loginMutationCalled = false
const variables = {
input: {
email: 'test#example.com',
password: '123',
},
}
const result = () => {
loginMutationCalled = true
return {
data: {
Login: {
accountId: '123',
},
},
}
}
const mocks = [
{
request: {
query: LOGIN_MUTATION,
variables,
},
result,
},
]
it('should call the login mutation', async () => {
await act(async () => {
const { findByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<LoginContainer />
</MockedProvider>,
)
fireEvent.change(await findByTestId('login-email-input'), {
target: {
value: 'test#example.com',
},
})
fireEvent.change(await findByTestId('login-password-input'), {
target: {
value: '123',
},
})
await wait(async () =>
fireEvent.click(await findByTestId('login-submit-input')),
)
})
expect(loginMutationCalled).toBe(true)
})
})
How do I make sure the loginMutation promise has been resolved before running the assertions?

Please check out my GitHub https://github.com/Arit143/mytube-ui/blob/master/src/pages/List.spec.tsx for async act wait. Usually, I wrap the act and wait in a function and then just wait for it to fulfill. If you feel the login resolve is taking much time, you can increase the wait amount. Just have a look at the GitHub URL as an example and let me know in case you still face an issue.

Related

How to implement a unit test of a component whcih uses a custom hook

I'm having a component that includes 2 switches of Material UI and I want to build a unit test of it.
The component uses data from an API to set the switches off/on default and also has the possibility for the user to click and set it on/off
The data comes and it is set by a custom hook which probably needs to be mocked but I have difficulties doing it having errors
The custom hook is this one
Custom Hook
The goal is to test 4 scenarios
The data comes in that way making the switches off
The data comes in that way making the switches on
The user clicks event to turn on the switches
The user clicks event to turn off the switches
I started by number 1 but don't know how to make it work and this is the unit test I started doing
/**
* #jest-environment jsdom
*/
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { render, screen } from 'test/app-test-utils';
import StudyConfiguration from './StudyConfiguration';
jest.mock('#lib/hooks/useConfiguration');
test.only('renders with SMS messaging and Email replay - all switches off disable', async () => {
jest
.spyOn({ useConfiguration }, 'useConfiguration')
.mockImplementationOnce(() => false)
.mockImplementationOnce(() => {value: 'noreply#test.com'})
.mockReturnValue([
{
scope: 'GLOBAL',
value: 'global#test.com',
},
{ scope: 'DEFAULT', value: 'hello#test.com' },
{ scope: 'STUDY', value: 'noreply#test.com' },
]);
const studyId = { studyId: 'study-1' };
render(<StudyConfiguration studyId={studyId} />);
const replyEmail = screen.getByTestId('email-reply');
const smsMessaging = screen.getByTestId('sms-enable');
// This throws an error if text not matching expect is no need it
screen.getByText(
/allow candidates to reply to emails \(send from global#test\.com instead of noreply#test\.com\)/i,
);
screen.getByText(/sms messaging/i);
expect(replyEmail.querySelector('input[type=checkbox]')).not.toBeChecked();
expect(smsMessaging.querySelector('input[type=checkbox]')).not.toBeChecked();
});
This gave the following errors inside the custom hook and I don't know the right way of mocking it
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
7 | const intl = useIntl();
8 |
> 9 | const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
| ^
10 | name: 'messaging.email.sender.address',
11 | scope: { studyId },
12 | });
The component
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { FormControlLabel, FormGroup, Switch, Typography } from '#mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const StudyConfiguration = ({ studyId }) => {
const intl = useIntl();
const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [smsSettings, setSmsSettings, unsetSmsSettings] = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
const [studyConfOverride, setStudyConfOverride] = useState(
emailSettings?.overrideChain,
);
const [replayEmailAddress, setReplayEmailAddress] = useState(
emailSettings?.value,
);
const [isSmsEnabled, setIsSmsEnabled] = useState(smsSettings?.value);
useEffect(() => {
if (studyConfOverride.length !== emailSettings?.overrideChain.length) {
setStudyConfOverride(emailSettings?.overrideChain);
}
}, [emailSettings?.overrideChain]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (replayEmailAddress !== emailSettings?.value) {
setReplayEmailAddress(emailSettings?.value);
}
}, [emailSettings?.value]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (isSmsEnabled !== smsSettings?.value) {
setIsSmsEnabled(smsSettings?.value);
}
}, [smsSettings?.value]); // eslint-disable-line react-hooks/exhaustive-deps
// Building the default reply email based on 'SCOPE'
// !TODO: study overrides sort in study service (TBD)
let defaultEmail;
if (studyConfOverride?.find(o => o.scope === 'GLOBAL')) {
const { value } = studyConfOverride.find(o => o.scope === 'GLOBAL');
defaultEmail = value;
} else if (studyConfOverride?.find(o => o.scope === 'DEFAULT')) {
const { value } = studyConfOverride.find(o => o.scope === 'DEFAULT');
defaultEmail = value;
}
// Extracting the email domain from default email and used to make a 'noreply#domain.xxx'
const emailDomain = defaultEmail?.substring(defaultEmail.indexOf('#'));
const noReplyEmail = `noreply${emailDomain}`;
const handleReplyEmailChange = async event => {
setReplayEmailAddress(event.target.checked ? defaultEmail : noReplyEmail);
event.target.checked
? await unsetSenderEmail()
: await setSenderEmail(noReplyEmail);
};
const handleSmsConf = async event => {
setIsSmsEnabled(event.target.checked);
event.target.checked
? await unsetSmsSettings()
: await setSmsSettings('false');
};
const isEmailEnabled = replayEmailAddress === defaultEmail;
return (
<FormGroup>
<FormControlLabel
control={
<Switch
data-testid="email-reply"
checked={isEmailEnabled}
onChange={handleReplyEmailChange}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage(
{
defaultMessage:
'Allow candidates to reply to emails (send from {replyEmailTxt} instead of {noReplyTxt})',
},
{ replyEmailTxt: defaultEmail, noReplyTxt: noReplyEmail },
)}
</Typography>
}
/>
<FormControlLabel
control={
<Switch
data-testid="sms-enable"
checked={isSmsEnabled}
onChange={handleSmsConf}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage({
defaultMessage: `SMS messaging`,
})}
</Typography>
}
/>
</FormGroup>
);
};
export default StudyConfiguration;
From the following section, we are calling the custom hook
const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [smsSettings, setSmsSettings, unsetSmsSettings] = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
I don't know how to mock that and if we console log the first values this is what we have
Email settings:: {
"value": "noreply#test.com",
"overrideChain": [
{
"__typename": "ConfigurationOverrideSlab",
"value": "noreply#test.com",
"scope": "STUDY"
},
{
"__typename": "ConfigurationOverrideSlab",
"value": "global#test.com",
"scope": "GLOBAL"
},
{
"__typename": "ConfigurationOverrideSlab",
"value": "hello#app.trialbee.com",
"scope": "DEFAULT"
}
]
}
I should be able to mock the email settings to show the resulting OBJ and check for that the switches are off.
The other 2 values are functions
setSenderEmail:: ƒ (value) {
return setStudyConfiguration({
variables: {
input: {
name: name,
scope: scope,
value: value
}
}
});
}
unSetSenderEmail:: ƒ () {
return unsetStudyConfiguration({
variables: {
input: _objectSpread({}, input)
}
});
}

When Reloading by Browser[F5] then,a state always will be undefined

state.firebase.profile always is undefined when I reload by browser.
Somehow, it goes well except for F5 as far as I can see.
I check by using console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));.
Where should I modify it...
class ReagtTagSample extends Component {
constructor(props) {
super(props);
this.state = {
porco:""
tags: [{ id: 'Yugoslavia', text: 'Yugoslavia' }, { id: 'India', text: 'India' }],
suggestions: [
{ id: "England", text: "England" },
{ id: "Mexico", text: "Mexico" },
],
};
componentDidMount=()=>{
console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));
}
handleAddition(tag) {
this.setState((state) => ({ tags: [...state.tags, tag] }));
}
handleDrag(tag, currPos, newPos) {
const tags = [...this.state.tags];
const newTags = tags.slice();
newTags.splice(currPos, 1);
newTags.splice(newPos, 0, tag);
this.setState({ tags: newTags });
}
//ommit
render() {
const { auth, authError, profile } = this.props;
return (
//ommit
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth,
authError: state.auth.authError,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = (dispatch) => {
return {
profileUpdate: (user) => dispatch(Update(user)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Update);
Update= (user) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore(); 
const firebase = getFirebase();
const profile = getState().firebase.profile;
const authorId = getState().firebase.auth.uid;
firestore.collection('users').doc(authorId).set({
name: user.userName,
tags:user.tags,
}).then(() => {
dispatch({ type: 'PROFILE_UPDATE_SUCCESS' })
}).catch(err => {
dispatch({ type: 'PROFILE_UPDATE_ERROR', err })
})
}
}
I would like to use profile.name as default input name...
<div className="input-field">
<label htmlFor="userName">DisplayName</label>
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
React state and props will be reset to their initial values when we reload the web app in browser using F5 or refresh button (because the app restarts as fresh).
The console log in componentDidMount prints undefined:
componentDidMount = () => {
console.log("TEST HERE" + JSON.stringify(this.props.profile.name));
// side node: you do not really need an arrow function here as it is a special
// lifecycle method. It will the `this` automatically binded with component instance
}
because, probably you are getting this.props.profile data through an API call. Hence, this.props.profile will receive its values asynchronously. You can see it on console log in componentDidUpdate lifecycle method.
Solution:
But if you want to set the default value of below input from this.props.profile.name, you can use either of these options:
Option 1: Using key and defaultValue. It will work because React components or elements re-render when their key is changed. And due to re-render it will read new defaultValue.
<input
key={this.props.profile.name}
defaultValue={this.props.profile.name}
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
Option 2: Set the userName in state when data is available in props:
componentDidUpdate(prevProps, prevState) {
if (this.props.profile.name !== prevProps.profile.name) {
this.setState({
userName: this.props.profile.name,
})
}
}
...
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>

How to show submitted data in React immediately, without refreshing the page?

I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).

props.actions `undefined` when using redux and react.js

I'm following a tutorial from this link: http://www.thegreatcodeadventure.com/react-redux-tutorial-part-ii-react-router-and-container-components/
But when the handleSubmit() function is fired i get an error:
TypeError: Cannot read property 'logInUser' of undefined
Indeed when i try to log this.props.actions it's undefined but i don't understand why. Is there something missing?
I'm using Antd as UI framework.
Component
import React, { Component } from 'react';
import { Form, Icon, Input, Button, Checkbox } from 'antd';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as sessionActions from './actions/sessionActions';
const FormItem = Form.Item;
class Login extends Component {
constructor(props){
super(props);
this.state = {credentials: {username: '', password: ''}};
this.handleSubmit = this.handleSubmit.bind(this);
this.onChange = this.onChange.bind(this);
}
onChange(event) {
const field = event.target.name;
const credentials = this.state.credentials;
credentials[field] = event.target.value;;
return this.setState({credentials: credentials});
}
handleSubmit = (event) => {
event.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
//console.log(this.props.actions);
this.props.actions.logInUser(this.state.credentials);
}
});
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form onSubmit={this.handleSubmit} className="login-form">
<FormItem>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'username missing!' }],
})(
<Input
name="username"
value={this.state.credentials.username}
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username o email"
onChange={this.onChange}/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Password missing!' }],
})(
<Input
name="password"
value={this.state.credentials.password}
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
onChange={this.onChange}/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: false,
})(
<Checkbox>Ricordami</Checkbox>
)}
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
</FormItem>
</Form>
);
}
}
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(sessionActions, dispatch)
};
}
export default Form.create()(Login);connect(null, mapDispatchToProps)(Login);
sessionReducer.js
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function sessionReducer(state = initialState.session, action) {
switch(action.type) {
case types.LOG_IN_SUCCESS:
this.context.history.push('/')
return !!sessionStorage.jwt
default:
return state;
}
}
sessionActions.js
import * as types from './actionTypes';
import sessionApi from '../api/sessionApi';
export function loginSuccess() {
return {type: types.LOG_IN_SUCCESS}
}
export function logInUser(credentials) {
return function(dispatch) {
return sessionApi.login(credentials).then(response => {
sessionStorage.setItem('jwt', response.jwt);
dispatch(loginSuccess());
}).catch(error => {
throw(error);
});
};
}
UPDATE
I fixed the problem with the help of #Chaim Friedman but now i got another error:
Error: Actions must be plain objects. Use custom middleware for async actions.
But i'm using redux-thunk as middleware. Here's login function if it can helps:
sessionApi.js
import React from 'react'
var axios = require('axios');
var qs = require('qs');
class SessionApi {
static login(credentials){
axios.post('http://localhost:5000/login', qs.stringify({auth: credentials}))
.then(response => {
console.log(response);
return response.json();
}),
error => {
console.log(error);
return error;
};
}
}
I believe your trouble is with this line here.
export default Form.create()(Login);connect(null, mapDispatchToProps)(Login);
You are only exporting what Form.create() returns, so therefor your component is not actually connected to redux.
To fix this issue you would need to do something like this.
export default Form.create(connect(null, matchDispatchToProps)(Login));
The exact syntax made be different, it would depend on the usage of Form.create(), but this would be the basic idea.

State is undefined when simulating click in react component test

I have been trying to test the behavior of my method handleFormSubmit(). When submit is clicked, the call should be triggered. This is working absolutely fine until I add the two lines listed below. It seems enzyme is updating the context or not calling the constructor?
Any help, much appreciated. I've stripped out what I can to keep the post brief.
The lines that fail - state is undefined
data.username = this.state.username.trim();
data.password = this.state.password.trim();
Component:
import React from 'react';
// Renders login form.
export default class LoginView extends React.Component {
static propTypes = {
model: React.PropTypes.object.isRequired,
params: React.PropTypes.object.isRequired
}
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
handleUsernameChange(event) {
this.setState({error: false, username: event.target.value});
}
handlePasswordChange(event) {
this.setState({error: false, password: event.target.value});
}
handleFormSubmit(event) {
event.preventDefault();
let failureMsg = {},
data = this.props.params,
options = {
success: (response) => {
if (response.attributes.successfulLogin) {
window.location = this.props.params.redirect +
'?authenticationToken=' + response.get('authenticationToken') +
this.createParams();
} else {
throw Error('Helpful error');
}
},
error: () => {
throw Error('Helpful error');
}
};
// PROBLEM LINES!
data.username = this.state.username.trim();
data.password = this.state.password.trim();
if (this.state.username && this.state.password && this.state.terms) {
this.props.model.save(data, options);
} else {
this.setState({
error: true,
errorMessage: !this.state.username || !this.state.password ? LoginConstants.LOGIN.BLANK_INPUT : LoginConstants.LOGIN.BLANK_TERMS
});
}
}
render() {
return (
<form>
<input type="text" ref="username" id="username" placeholder="Customer ID" onChange={this.handleUsernameChange.bind(this)} maxLength={75} value={this.state.username} />
<input type="password" ref="password" id="password" placeholder="Password" onChange={this.handlePasswordChange.bind(this)} maxLength={75} value={this.state.password} />
<button type="submit" ref="login" id="login" className="Button Button--primary u-marginT-md" onClick={this.handleFormSubmit.bind(this)}>LOGIN</button>
</form>
);
}
}
Unit test:
import React from 'react';
import { mount } from 'enzyme';
import LoginView from './LoginView';
describe('Login screen', () => {
let mountedComp;
beforeAll(() => {
mountedComp = mount(<LoginView />);
});
it('should show error if submitted blank', () => {
mountedComp.instance().state = {
username: '',
password: ''
};
expect(mountedComp.find('#js-errorMessage').length).toEqual(0);
mountedComp.ref('login').simulate('click', { preventDefault: () => undefined, state: {} });
expect(mountedComp.find('#js-errorMessage').length).toBeGreaterThan(0);
});
});
try binding your onClick function in
<form onClick={this.handleFormSubmit.bind(this)} >
Looks like your "this.state: is out of scope. consider adding
var _self = this;
and use
_self.state.username.trim();
_self.state.password.trim();
I think the source of the problem may lay in this very line:
let data = this.props.params
In JavaScript it will not copy this.props.params object to data object by value, but by reference. It will set the same reference for data and this.props.params – which means if you change contents of data it will be reflected in this.props.params and vice-versa.
So you're actually mutating props which are supposed to be immutable.
You should rather create a shallow copy, e.g. like that:
let data = Object.assign({}, this.props.params)
I managed to get around this by passing my params manually to my jsx object. I'm not sure this is the proper way to test, but it worked for now.
<LoginView model={model} params={params} />

Categories

Resources