Test the state in parent component that was updated thru child component - reactjs

I have a function called onFormSubmit in parent component. I pass this function to child component. Upon form submit in child component, onFormSubmit function inside child component is called to pass a value back to parent component. Then this onFormSubmit function does some kind of checking and based on that, updates the state in parent component.
I want to mock/stub out this ajax/api call. How can I achieve this? Or how do I write my code in such a way that this scenario is testable.
My Parent component looks like this:
class App extends React.Component {
state: { message: "" }; //I want to test the state of message
onFormSubmit = async (form) => {
if (form.primaryEmail !== "") {
const response = await axios.post("api/", form);
this.setState(
{
message: (response.status === 200) ? "Your email has been updated." : ""
});
} else {
this.setState({ message: "No Update" });
}
}
render() {
return (
<div>
<Child onSubmit={this.onFormSubmit} />
<h4>{this.state.message}</h4>
</div>
);
}
}
My Child Component looks like this:
class Child extends React.Component {
state = {
primaryEmail: "",
};
onPrimaryEmailChange = e => {
this.setState({ primaryEmail: e.target.value });
}
onFormSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state); //passing the value back to parent component
}
render() {
return (
<form onSubmit={this.onFormSubmit}>
<h3>Email Address</h3>
<div>
<input type="email" value={this.state.primaryEmail} onChange={this.onPrimaryEmailChange} />
</div>
<div>
<button type="submit">Submit</button>
</div>
</form >
);
}
}
My test looks like this:
test("When valid form is submitted, it should show a success message", () => {
const wrapper = mount(<App />);
wrapper.find("input").at(0).simulate("change", {
target: {
value: "a#b.c",
}
});
wrapper.find('form').simulate('submit');
expect(wrapper.state('message')).toEqual('Your email has been updated.');
});
I get this error:
Expected value to equal:
"Your email has been updated."
Received:
""

As it happens, I ran into a similar situation earlier this week. Here's how I solved it. There may be better solutions, but this worked in my situation.
Disclaimer: I'm writing this from memory directly into the StackOverflow answer field, so it might not be 100% accurate.
First, you should mock Axios so you can have control over the API's output for your tests. You should never actually perform an HTTP request from a test case, because you're not testing your API -- you're testing how your component responds to a particular API response. My project uses create-react-app, which configures Jest to load mocks from the __mocks__ folder in the root of the project.
__mocks__/axios.js:
export default {
// since you are only testing "axios.post()", I'm only mocking "post"
post: jest.fn()
}
Then in your parent component's test, you can specify a mock implementation for the post function that returns a 200 response (that's the case you're testing).
__tests__/App.test.jsx:
// in Jest context, this should load from __mocks__/axios.js
import axios from "axios";
test("When valid form is submitted, it should show a success message", () => {
// Axios itself returns a Promise, so the mock should as well
axios.post.mockImplementationOnce(
(url, formData) => Promise.resolve({
status: 200
})
);
const wrapper = mount(<App />);
// Optionally, test the default state to be an empty string
expect(wrapper.state()).toHaveProperty("message");
expect(wrapper.state().message).toBe("");
wrapper.find("input").at(0).simulate("change", {
target: {
value: "a#b.c",
}
});
wrapper.find("form").simulate("submit");
// Optionally, test if Axios was called
expect(axios.post).toHaveBeenCalled();
// More optionally, test if it was called with the correct email address
expect(axios.post).toHaveBeenCalledWith(
expect.any(),
expect.objectContaining({ primaryEmail: "a#b.c" })
);
// Note that even though Axios may have been called, the asynchronous
// Promise may not have completed yet which means the state will not
// have been updated yet. To be safe, let's use setImmediate(), which
// should wait for any pending Promises to complete first
setImmediate(async () => {
// Now we can test that the state has changed
expect(wrapper.state().message).toBe("Your email has been updated.");
});
});

Related

How to store the value of a API response in a state?

I want to store the value obtained after the api call in the state so that it can be rendered later on.I want the search term to be fetched from the api,i tried to use compontdidMount and conditionally fetch from the api but it didn't work as well.please help
import React, { Component } from 'react'
import axios from 'axios';
class New extends Component {
state = {
term:'',
res:[]
}
handleChange = e => {
this.setState({ term: e.target.value });
};
handleSubmit = e => {
e.preventDefault();
var url = `https://icanhazdadjoke.com/search?term=${this.state.term}`;
var options = {
method: 'GET',
headers: {
"Accept" : "application/json"
}
};
axios.get(url,options)
.then(response =>
this.setState({res:[response.data.results]})
)
.catch(error => {
// error.message is the error message
})
console.log(this.state)
};
render() {
return (<form className="jokes" onSubmit={this.handleSubmit}>
<input
onChange={this.handleChange}
value={this.state.title}
type="text"
name="term"
required
placeholder="cemetery"
/>
<button type="submit" value="Search" >Search</button>
</form> );
}
}
export default New;
Working demo, provided that you search a joke term that exists.
Just two observations:
Setting the state like this this.setState({res:[response.data.results]}) will make the response be inside an array with only the zeroth element. But response.data.results is already an array, so just save this.setState({res:response.data.results})
this.setState does not take effect right away and logging in the next line won't work. The state change is queued and async... you are asking React to update when it can.
EDIT:
setState has a secondary and optional callback argument, that executes after the actual update:
this.setState({ res: [response.data.results] }, () => {
console.log(this.state.res);
})
It appears as though the state does get updated correctly. Check out this sandbox where I demo your code and render the API results.

I wanna console.log the value after clicking the submit button once and to delete the previous mapped items, but it doesnt work

I'm very new to react and I got two problems:
I want to console log the input and display the mapped data after clicking the submit button once. But I get console logged the input and the mapped data after clicking the button twice.
I wanna clear the mapped list (data from previous input) and display new list items depending on the input. But the new list items are only added to the end of the previous list (only the last list item from the previous list got overwritten by the first list item of the new list).
So this is the code from my app component:
import React, { Component, Fragment } from 'react';
import './App.css';
import Display from './/Display';
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: "",
passedValue: ""
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
handleSubmit(event) {
this.setState({ passedValue: this.state.value });
console.log(this.state.passedValue);
event.preventDefault();
}
render() {
return (
<div>
<form className="inputContainer" onSubmit={this.handleSubmit}>
<input type="text" name="company_name" onChange={this.handleChange} />
<input type="submit" value="Submit" />
</form>
<Display listDataFromParent={this.state.passedValue} />
</div>
);
}
}
export default App;
And this is my display component:
import React, { Component } from 'react'
import "./Display.css";
export default class Display extends Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
data: []
};
}
componentWillReceiveProps() {
fetch("http://localhost:5000/company?company_name=" + this.props.listDataFromParent)
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
data: result
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, data } = this.state;
// if (error) {
// return <div>Error: {error.message}</div>;
// } else if (!isLoaded) {
// return <div>Loading...</div>;
// } else {
return (
<div className="display">
<h1>Kreditnehmer</h1>
<ul>
{this.props.listDataFromParent}
{data.map(item => (
<li key={item.c.company_id}>
Relation type: {item.r.relation_group}
Last name: {item.p.last_name}
</li>
))}
</ul>
</div>
);
}
}
Can anyone help?
1) setState is async method in react means it will take some time to update the component state. You can get your console log by using callback function of setState like
this.setstate({ value: e.target.value }, () => { console.log(this.state.value) });
2) in display component, your using componentWillReciveProps life cycle and inside that your using this.props.listdatafromparent which is pointing previous props. Rather than using this.props I would suggest consider props param of life cycle, means it should be like
componentWillReciveProps(props) {
// your code
Console.log(props.listdatafromparent);
}
The handleSubmit method is wrong... the console log is executed before the state is changed. You need to put it inside a callback function as a second parameter of setState.
this.setState({ passedValue: this.state.value }, () => {
console.log(this.state.passedValue);
});
Answers are:
1) Callback function should be used on setState, in order to do console.log after state is really updated.
In your case you call setState and setState is async function, which means that console.log won't wait until state is really updated.
Your code should be:
handleSubmit(event) {
this.setState({ passedValue: this.state.value },
() => console.log(this.state.passedValue));
event.preventDefault();
}
2) I would move data fetching out of componentWillReceiveProps(), since this lifecycle method will be deprecated from version 17 and it is fired on every render(). Try replacing with componentDidMount() or componentDidUpdate(). Maybe just this small change will solve your problem. If not pls post results and I will take a look again.

How to change a component's state correctly from another component as a login method executes?

I have two components - a sign in form component that holds the form and handles login logic, and a progress bar similar to the one on top here in SO. I want to be able to show my progress bar fill up as the login logic executes if that makes sense, so as something is happening show the user an indication of loading. I've got the styling sorted I just need to understand how to correctly trigger the functions.
I'm new to React so my first thought was to define handleFillerStateMax() and handleFillerStateMin() within my ProgressBarComponent to perform the state changes. As the state changes it basically changes the width of the progress bar, it all works fine. But how do I call the functions from ProgressBarComponent as my Login component onSubmit logic executes? I've commented my ideas but they obviously don't work..
ProgressBarComponent:
class ProgressBarComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
percentage: 0
}
}
// the functions to change state
handleFillerStateMax = () => {
this.setState ({percentage: 100})
}
handleFillerStateMin = () => {
this.setState ({percentage: 0})
}
render () {
return (
<div>
<ProgressBar percentage={this.state.percentage}/>
</div>
)
}
}
Login component:
class SignInFormBase extends Component {
constructor(props) {
super(props);
this.state = {...INITIAL_STATE};
}
onSubmit = event => {
const {email, password} = this.state;
// ProgressBarComponent.handleFillerMax()????
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
//ProgressBarComponent.handleFillerMin()????
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
Rephrase what you're doing. Not "setting the progress bar's progress" but "modifying the applications state such that the progress bar will re-render with new data".
Keep the current progress in the state of the parent of SignInFormBase and ProgressBarComponent, and pass it to ProgressBarComponent as a prop so it just renders what it is told. Unless there is some internal logic omitted from ProgressBar that handles its own progress update; is there?
Pass in a callback to SignInFormBase that it can call when it has new information to report: that is, replace ProgressBarComponent.handleFillerMax() with this.props.reportProgress(100) or some such thing. The callback should setState({progress: value}).
Now, when the SignInFormBase calls the reportProgress callback, it sets the state in the parent components. This state is passed in to ProgressBarComponent as a prop, so the fact that it changed will cause he progress bar to re-render.
Requested example for #2, something like the following untested code:
class App extends Component {
handleProgressUpdate(progress) {
this.setState({progress: progress});
}
render() {
return (
<MyRootElement>
<ProgressBar progress={this.state.progress} />
<LoginForm onProgressUpudate={(progress) => this.handleProgressUpdate(progress)} />
</MyRootElemen>
)
}
}
The simply call this.props.onProgressUpdate(value) from LoginForm whenever it has new information that should change the value.
In basic terms, this is the sort of structure to go for (using useState for brevity but it could of course be a class-based stateful component if you prefer):
const App = ()=> {
const [isLoggingIn, setIsLoggingIn] = useState(false)
const handleOnLoginStart = () => {
setIsLoggingIn(true)
}
const handleOnLoginSuccess = () => {
setIsLoggingIn(false)
}
<div>
<ProgressBar percentage={isLoggingIn?0:100}/>
<LoginForm onLoginStart={handleOnLogin} onLoginSuccess={handleOnLoginSuccess}/>
</div>
}
In your LoginForm you would have:
onSubmit = event => {
const {email, password} = this.state;
this.props.onLoginStart() // <-- call the callback
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
this.props.onLoginSuccess() // <-- call the callback
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}

How to work with async server calls in Reactjs 16.7.0

What is the correct way to use async methods as props in ReactJs? I have an app which mounts a component "dashboard" and this component then loads two drop down menus. The first drop down is populated by async fetch at the componentDidMount lifecycle method.
When the first drop down changes, the async event handler is triggered and the state property selectedAId is supposed to be set. If I debug this in the browser I do see this state prop set. After the state is set, the same event handler also calls an async server method to get some data based on the state property selectedAId.
If I then go to the async server method getEntitledSites, the parameter sent to the method is "0", instead of the value that I previously saw being set in the event handler.
Here is the gist to a summary of the code:
https://gist.github.com/thehme/c4b5a958ef1a6e5248f697375c9cb84b#file-api-ts-L15
api.ts
class Api {
...
async getEntitledForA() {
try {
const orgsResponse = await fetch('/api/v1/orgs');
const orgResponseJson = await orgsResponse.json();
return orgResponseJson.data;
} catch (err) {
console.error(err);
return null;
}
}
async getEntitledSites(orgId: number) {
try {
const sitesResponse = await fetch('/api/v1/study/' + orgId + '/sites');
const sitesResponseJson = await sitesResponse.json();
return sitesResponseJson;
} catch (err) {
console.error(err);
return null;
}
}
}
export default new Api();
dashboard.jsx
import React, {Component} from 'react';
import SelectDropDownA from '../components/SelectDropDownA';
import SelectDropDownB from '../components/SelectDropDownB';
import SelectDropDownC from '../components/SelectDropDownC';
class CreateDashboard extends Component {
constructor() {
super();
this.state = {
organizations: [],
sites: [],
selectedAId: "0",
selectedBId: "0",
selectedCId: "0",
showBMenu: false
}
}
async componentDidMount() {
let organizations = await Api.getEntitledForA();
this.setState({ organizations });
}
async onSelectedOrgChange(selectedOrgId) {
if (selectedOrgId !== 0) {
this.setState({ selectedAId });
let sitesData = await Api.getEntitledSites(this.state.selectedAId);
this.setState({
sites: sitesData.sites,
showSitesMenu: true
});
}
}
...
render() {
return (
<div className="panel">
<div className="insidePanel">
<SelectStudyDropDown
onChange={e => this.onSelectedOrgChange(e.target.value)}
...
/>
</div>
{this.showSitesMenu &&
<div className="insidePanel">
<SelectSiteDropDown
onChange={e => this.onSelectedSiteChange(e.target.value)}
...
/>
</div>
}
</div>
)
}
}
export default CreateDashboard;
I am wondering if this has to do with how I am using async/await or the binding of the onChange method.
SOLUTION: In my particular case, I do not need this.state to set selectedAId before starting my server request, so by passing the parameter straight to the request, works fine. Then I can simply move the setting of selectedAId to the second instance of this.setState. This would not work, if I needed selectedAId to be set before making the server request, in which case, I would have to wait for this.state to finish being set, but this isn't the case since I want my server request to start right away.
async onSelectedOrgChange(selectedOrgId) {
if (selectedOrgId !== 0) {
let sitesData = await Api.getEntitledSites(selectedOrgId);
this.setState({
selectedAId,
sites: sitesData.sites,
showSitesMenu: true
});
}
}
this.setState is asynchronous and not awaitable (i.e. does not return a promise), so if you want to do something with the new state after it has been set, you need to do it in a callback:
this.setState({ selectedAId }, async () => {
let sitesData = await Api.getEntitledSites(this.state.selectedAId);
this.setState({
sites: sitesData.sites,
showSitesMenu: true
});
});
Additionally, in your example, onSelectedOrgChange isn't binded to this anywhere. However if this was an issue the whole thing would have broken long before so I suppose that you just left it out by accident?
Furthermore I would advise you to extract the e.target.value inside the handler and pass onChange={this.onSelectedOrgChange} as the prop, because in your current code you're creating a new onChange handler every time render is called.

How can I mock the window.alert method in jest?

I have the following React component:
class Form extends React.Component {
constructor(props) {
super(props);
this.state = this._createEmptyTodo();
}
render() {
this.i18n = this.context;
return (
<div className="form">
<form onSubmit={this._handleSubmit.bind(this)}>
<input
placeholder={this.i18n.placeholders.addTitle}
type="text"
value={this.state.title}
onChange={this._handleTitleChange.bind(this)}></input>
<textarea
placeholder={this.i18n.placeholders.addDescription}
value={this.state.description}
onChange={this._handleDescriptionChange.bind(this)}></textarea>
<button>{this.i18n.buttons.submit}</button>
</form>
</div>
);
}
_handleTitleChange(e) {
this.setState({
title: e.target.value
});
}
_handleDescriptionChange(e) {
this.setState({
description: e.target.value
});
}
_handleSubmit(e) {
e.preventDefault();
var todo = {
date: new Date().getTime(),
title: this.state.title.trim(),
description: this.state.description.trim(),
done: false
};
if (!todo.title) {
alert(this.i18n.errors.title);
return;
}
if (!todo.description) {
alert(this.i18n.errors.description);
return;
}
this.props.showSpinner();
this.props.actions.addTodo(todo);
this.setState(this._createEmptyTodo());
}
_createEmptyTodo() {
return {
"pkey": null,
"title": "",
"description": ""
};
}
}
And the related test:
const i18nContext = React.createContext();
Form.contextType = i18nContext;
describe('The <Form> component', () => {
var wrapper;
var showSpinner;
var actions = {}
beforeEach(() => {
showSpinner = jest.fn();
actions.addTodo = jest.fn();
wrapper = mount(<i18nContext.Provider value={i18n["en"]}>
<Form
showModalPanel={showSpinner}
actions={actions} />
</i18nContext.Provider>);
});
test("validate its input", () => {
window.alert = jest.fn();
wrapper.find("button").simulate("click");
expect(window.alert.mock.calls.length).toBe(1);//<<< this FAILS!
});
});
This form, when the button gets clicked, it simply alerts a message using alert.
Now when I run the test I get this:
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
Which is a failure because the mock does not get called apparently. But I promise you that the form component does alert a message when clicking on its button.
I suspect that, for some reasons, the mocked window.alert does not get used by the Form component when the click is performed programmatically using enzyme.
Anyone?
In Jest configuration with JSDOM global.window === global, so it can be mocked on window.
It's preferable to mock it like
jest.spyOn(window, 'alert').mockImplementation(() => {});
because window.alert = jest.fn() contaminates other tests in this suite.
The problem with blackbox testing is that troubleshooting is harder, also relying on the behaviour that expected from real DOM may cause problems because Enzyme doesn't necessary support this behaviour. It's unknown whether the actual problem, handleSubmit was called or not, that alert mock wasn't called is just an evidence that something went wrong.
In this case click event on a button won't cause submit event on parent form because Enzyme doesn't support that by design.
A proper unit-testing strategy is to set up spies or mocks for all units except tested one, which is submit event handler. It usually involves shallow instead of mount.
It likely should be:
jest.spyOn(window, 'alert').mockImplementation(() => {});
const formWrapper = wrapper.find(Form).dive();
jest.spyOn(formWrapper.instance(), '_handleSubmit');
formWrapper.find("form").simulate("submit");
expect(formWrapper.instance()._handleSubmit).toBeCalled();
expect(window.alert).toBeCalledWith(...);
State should be changed directly with formWrapper.setState instead of DOM events simulation.
A more isolated unit test would be to assert that form was provided with expected onSubmit prop and call formWrapper.instance()._handleSubmit(...) directly.
Instead of window, you can use global.
global.alert = jest.fn();
This is because browsers use the window name, while nodejs use the global name.

Resources