I'm trying to test a registration component that has a Vertical Stepper with Jest/Enzyme and I keep hitting a wall when trying to simulate the user clicking "Next" .
expected behavior is to do nothing if the "Required" input fields are empty, however after doing the .simulate('click') following assertions fail with not finding any html in the wrapper.
The component is passed through react-redux connect() so I don't know if that would be related.
UserRegistration.js
import React from 'react';
import { connect } from 'react-redux';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step;
import StepLabel from '#material-ui/core/StepLabel;
import StepContent from '#material-ui/core/StepContent'
class UserRegistration extends React.Component {
constructor(props){
this.state = {
activeStep: 0,
inputData: {},
...
}
}
getStepContent = () => {
switch(this.state.activeStep)
case '...':
return
(<>
<input test-data="firstName"/>
...
</>);
...
}
render () {
const steps = ['Personal Info', 'Corporate Details', ...]
return (
<Stepper activeStep={this.state.activeStep} orientation="vertical">
{steps.map((label, index) => {
return (
<Step key={index}/>
<StepLabel>{label}</StepLabel>
<StepContent>
{this.getStepContent()}
<button data-test="btn-next" onClick={() => this.goNext()}> NEXT </button>
<button onClick={() => this.goBack()}> BACK </button>
)
}
}
</Stepper>
)
}
}
const mapStateToProps = () => {...}
const mapDispatchToProps = () => {...}
export default connect(mapStateToProps, mapDispatchToProps)(UserRegistration)
UserRegistration.test.js
const wrapper = mount(
<Provider store={store}
<UserCreate/>
</Provider>
)
it('Confirm REQUIRED fields rendered', () => {
expect(wrapper.find("input[data-test='firstName']").length).toEqual(1);
// PASS
});
it('Check if still on same step clicked NEXT with no user data', () => {
wrapper.find("button[data-test='btn-next']").simulate('click');
expect(wrapper.find("input[data-test='firstName']").length).toEqual(1);
// Expected value to equal: 1, Received: 0
})
Same outcome regardless of the element I'm looking up.
Any suggestions will be greatly appreciated.
You need to update. So you would change it:
it('Check if still on same step clicked NEXT with no user data', () => {
wrapper.find("button[data-test='btn-next']").simulate('click');
// Add this line
wrapper.update();
const button = wrapper.find("input[data-test='firstName']");
expect(button.length).toEqual(1);
// Expected value to equal: 1, Received: 0
});
Then the test should work as you intend.
Related
I have a Button wrapper component in which I am using mui button. I want to do the unit testing for this button wrapper component. I wrote some code but for onClick test it is falling.
index.tsx (ButtonWrapper Component)
import React from 'react';
import { Button } from '#material-ui/core';
import { ButtonProps } from '../../../model';
type ConfigButtonProps ={
// variant?: string,
// color?: string,
// fullWidth?: boolean,
// type?: string
}
export const ButtonWrapper = (props: ButtonProps) => {
const {children, onSubmit, disabled, type, onClick, ...otherprops} = props
console.log("button", otherprops, type);
const configButton:ConfigButtonProps = {
variant: 'contained',
color: 'primary',
fullWidth: true,
type: type
}
return (
<Button disabled={disabled} onClick={onSubmit} {...configButton}>
{children}
</Button>
);
};
index.test.tsx (Button Wrapper test)
import { ButtonWrapper } from "./index"
import { fireEvent, render, screen } from "#testing-library/react";
import { ButtonProps } from "../../../model";
const makeSut = (props: ButtonProps) => {
return render(<ButtonWrapper onClick={jest.fn()} {...props} />);
};
describe("<ButtonWrapper />", () => {
test("Button renders correctly",()=>{
render(<ButtonWrapper />)
const buttonElem = screen.getByRole('button')
expect(buttonElem).toBeInTheDocument()
})
test("Should call onClick successfully", () => {
const spy = jest.fn();
const { getByRole } = makeSut({ onClick: spy });
fireEvent.click(getByRole('button'));
expect(spy).toHaveBeenCalled();
});
});
FormContainer.tsx (Parent Container)
return (<form onSubmit={event=>this.onSubmit(event)}>
{/* <div>FormContainer {JSON.stringify(this.props.states, null, 2)}</div> */}
{this.state.fields.map((field,index)=>{
return <FormControl
key={field.id}
fieldConfig={field}
focused={(event:React.ChangeEvent<HTMLInputElement>)=>this.fieldBlur(event,field,index)}
changed={(event:React.ChangeEvent<HTMLInputElement>)=>this.fieldChange(event,field,index)} />
})}
<ButtonWrapper type='submit'>Submit</ButtonWrapper>
</form>)
Error
I also want to know in order to make 90% test coverage what else I need to test ?
[![enter image description here]]
I tried this below mentioned code also but the last line fails.
test("Should call onClick successfully", () => {
const onSubmitHandler = jest.fn();
render(<ButtonWrapper onClick={onSubmitHandler} />)
const buttonElement = screen.getByRole('button');
user.click(buttonElement)
expect(onSubmitHandler).toHaveBeenCalledTimes(1) //This line fails
});
Your spy function is coming in onClick, not the onSubmit prop:
const { getByRole } = makeSut({ onClick: spy });
And your component assigns the value from onSubmit to onClick:
<Button disabled={disabled} onClick={onSubmit} {...configButton}>
{children}
</Button>
About the coverage - I cannot see your coverage report but what I can see in your component - there is a disabled prop. You should render the component with true and false values for this prop. This should give you higher coverage.
Below is the snippet for Homepage with its unit test in jest, which will display error message if state.includes('error') and display welcome message if state.includes('success').
while I run a unit test for Homepage component, beginning state is initial and then always changes to error skipping success state without any reason, which fails the unit test. Is there a way to inject the state inside the Apptest in the unit test so that it always get success state and skip error state.
log for state
console.log
<----- State is -------> initial
console.log
<----- State is -------> homePage.error
Can we mock the state so that new state is always homepage.success?
import React from 'react';
const Homepage = props => {
const { appState, globalDispatch, globalStore } = props;
const { store, transition, state } = appState;
const { welcomeMessage } = globalStore;
//useEffect hook to check the state
useEffect(() => {
console.log('<----- State is ------->', state);
});
return(
<React.Fragment>
{state.includes('error') ? (
<p>
There was error processing your request
</p>
): null}
{state.includes('success') ? (
<p>
{welcomeMessage}
</p>
): null}
</React.Fragment>
)
}
And the unit test for above component is
import React from 'react';
import { IntlProvider } from 'react-intl';
const AppTest = props => {
const { additionalData } = props;
const { globalDispatch, globalStore } = useMothershipContext();
const { theme } = useTheme({
globalStore,
globalDispatch
});
return (
<ThemeProvider theme={theme}>
<IntlProvider>
<Homepage
globalDispatch={globalDispatch}
globalStore={globalStore}
additionalData={additionalData}
/>
</IntlProvider>
</ThemeProvider>
);
};
beforeEach(() => {
jest.setTimeout(60000);
});
test('Stage 1.0 - Display Welcome Message', async () => {
const { getByText } = render(
<TestWrapper>
<AppTest additionalData={'Welcome Lorem Ipsum'} />
</TestWrapper>
);
await waitFor(() => getByText('Welcome Lorem Ipsum'));
const welcomeMessage = getByText('Welcome Lorem Ipsum');
expect(welcomeMessage).toBeTruthy();
});
And machine is
import { Machine } from 'xstate';
export default Machine({
id: 'homePage',
initial: 'initial',
states: {
initial: {
on: {
INITIALIZE_HOMEPAGE_SUCCESS: 'success',
INITIALIZE_HOMEPAGE_ERROR: 'error'
}
}
}
})
I'm building a chat app, I have 3 main components from parent to child in this hierarchical order: Chat.js, ChatLine.js, EditMessage.js.
I have a function updateMessage in Chat component that I need to pass to the second child EditMessage, but it causes a rerender of every ChatLine when I click on Edit button and begin typing.
I can't figure out how to memoize it so it only causes a rerender on the ChatLine I'm editing.
It only works if I pass it to ChatLine as :
updateMessage={line.id === editingId ? updateMessage : null}
instead of :
updateMessage={updateMessage}
But I want to avoid that and memoize it instead so it doesn't cause a rerender after each letter I type while editing a message.
This is the whole code: (also available in CodeSandbox & Netlify)
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={line.id === editingId ? editValue : ""}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
updateMessage={updateMessage}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
updateMessage={updateMessage}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
Use the useCallback React hook to memoize the updateMessage callback so it can be passed as a stable reference. The issue is that each time Chat is rendered when editValue state updates it is redeclaring the updateMessage function so it's a new reference and triggers each child component it's passed to to rerender.
import { useCallback } from 'react';
...
const updateMessage = useCallback(
(editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
// If updating messages state use functional state update to
// avoid external dependencies.
},
[]
);
I'm new to React and am tripping over this issue.
Have read couple of tutorials and questions here to find out about how Parent & Child Components should communicate. However, I am unable to get the data to populate the fields + make it editable at the same time. I'll try explain further in code below:
Parent Component:
...imports...
export default class Parent extends Component {
constructor(props) {
this.state = {
data: null
};
}
componentDidMount() {
API.getData()
.then((response) => {
this.setState({ data: response });
// returns an object: { name: 'Name goes here' }
})
}
render() {
return (
<Fragment>
<ChildComponentA data={this.state.data} />
<ChildComponentB data={this.state.data} />
</Fragment>
);
}
}
Input Hook: (source: https://rangle.io/blog/simplifying-controlled-inputs-with-hooks/)
import { useState } from "react";
export const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
setValue,
reset: () => setValue(""),
bind: {
value,
onChange: event => {
setValue(event.target.value);
}
}
};
};
ChildComponent:* (This works to allow me to type input)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput('');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to bind API data - Input still editable but data is still not populated even though it is correctly received.. The API data takes awhile to be received, so the initial value is undefined)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to use useEffect to bind the data works but input field cannot be typed..)
I believe this is because useEffect() is trigged every time we type.. and props.data.name is rebinding its original value
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
useEffect(() => {
if(props.data) {
setValue(props.data.name);
}
});
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
I can think of a few tricks like making sure it binds only once etc.. But I'm not sure if it is the correct approach. Could someone share some insights of what I could be doing wrong? And what should be the correct practice to do this.
To iterate, I'm trying to bind API data (which takes awhile to load) in parent, and passing them down as props to its children. These children have forms and I would like to populate them with these API data when it becomes available and yet remain editable after.
Thanks!
Basic way to create your Parent/Child Component structure is below, I believe. You don't need a class-based component for what you are trying to achieve. Just add an empty array as a second argument to your useEffect hook and it will work as a componentDidMount life-cycle method.
Parent component:
import React, {useState, useEffect} from 'react';
export default const Parent = () => {
const [data, setData] = useState({});
const [input, setInput] = useState({});
const inputHandler = input => setInput(input);
useEffect(() => {
axios.get('url')
.then(response => setData(response))
.catch(error => console.log(error));
}, []);
return <ChildComponent data={data} input={input} inputHandler={inputHandler} />;
};
Child Component:
import React from 'react';
export default const ChildComponent = props => {
return (
<div>
<h1>{props.data.name}</h1>
<input onChange={(e) => props.inputHandler(e.target.value)} value={props.input} />
</div>
);
};
I have a component that renders a button if a property errorMessage is not null.
class App extends Component {
static propTypes = {
// Injected by React Redux
errorMessage: PropTypes.string,
resetErrorMessage: PropTypes.func.isRequired,
};
renderErrorMessage() {
const { errorMessage } = this.props;
if (!errorMessage) return null;
return (
<p id="error-message">
<b>{errorMessage}</b>{' '}
<button id="dismiss" onClick={this.props.resetErrorMessage()}>
Dismiss
</button>
</p>
);
}
render() {
return (
<div className="app">
{this.renderErrorMessage()}
</div>
);
}
}
The property injected by React Redux:
import { connect } from 'react-redux';
import App from '../components/App/App';
const mapStateToProps = (state, ownProps) => ({
errorMessage: state.errorMessage,
});
export default connect(mapStateToProps, {
resetErrorMessage: () => ({
type: 'RESET_ERROR_MESSAGE',
})
})(App);
As you can see I also have resetErrorMessage that clears errorMessage:
const errorMessage = (state = null, action) => {
const { type, error } = action;
if (type === RESET_ERROR_MESSAGE) {
return null;
} else if (error) {
return error;
}
return state;
};
How can I test my component and say if I click the button then button hides or if errorMessage is not null button shows?
I want to get something like this:
const props = {
errorMessage: 'Service Unavailable',
resetErrorMessage,
};
it('renders error message', () => {
const wrapper = shallow(<App {...props} />);
expect(wrapper.find('#error-message').length).toBe(1);
wrapper.find('#dismiss').simulate('click');
expect(wrapper.find('#error-message').length).toBe(0);
});
But now my problem is that if I simulate click to dismiss button - error message doesn't hide.
As I posted in the previous question you deleted, if you want to test button clicks your best bet would be to call the 'unconnected' component. If you want to test the connected component, then you have to pass a mockstore into it like so.
const wrapper = shallow(<App {...props} store={store} />);
So import the app in your test and just pass the resetErrorMessage function as a mocked function, such as what you do with jest.
const resetErrorMessage = jest.fn(() => {});
const wrapper = shallow(<App {...props} resetErrorMessage={resetErrorMessage} />);
wrapper.find('#dismiss').simulate('click');
expect(resetErrorMessage).toHaveBeenCalled();
My advice would be to only test the connected component when you want to manipulate directly from store changes.