How test input onChange event with enzyme? - javascript

I made TextInput component, here is code:
export const TextInput = (props: ITextInputProps): TReactElement => {
const {
errorMessage,
hasError,
...restProps
} = props;
return (
<div>
<input
{ ...restProps }
type="text"
className={ mergeClassNames([
textInputStyles["text-input"],
hasError ? textInputStyles["text-input--error"] : "",
]) }
/>
{
hasError &&
<p className={ textInputStyles["text-input__error-message"] }>{ errorMessage }</p>
}
</div>
);
};
Now I wont test that onChange work correctly, I do it like this:
test("TextInput: should change value", () => {
let actualInputValue;
const textInputProps = {
onChange: (event: ChangeEvent<HTMLInputElement>): void => {
actualInputValue = event.currentTarget.value;
},
};
const textInputWrapper = shallow(<TextInput { ...textInputProps } />);
textInputWrapper.find(".text-input")
.simulate("change", {
currentTarget: {
value: "Hello, world!",
},
});
expect(actualInputValue)
.toBe("Hello, world!");
});
I feel that actualInputValue and onChange handler is excess because I can get value directly from .text-input
I tried read value like this (but got undefined):
test("TextInput: should change value", () => {
const textInputWrapper = shallow(<TextInput />);
textInputWrapper.find(".text-input")
.simulate("change", {
currentTarget: {
value: "Hello, world!",
},
});
expect(textInputWrapper.find(".text-input").props().value)
.toBe("Hello, world!");
});
Then I tried update textInputWrapper like this (but got undefined):
test("TextInput: should change value", () => {
const textInputWrapper = shallow(<TextInput />);
textInputWrapper.find(".text-input")
.simulate("change", {
currentTarget: {
value: "Hello, world!",
},
});
textInputWrapper.update();
expect(textInputWrapper.find(".text-input").props().value)
.toBe("Hello, world!");
});
Then I also tried use done callback (but got undefined):
test("TextInput: should change value", (done: () => void) => {
const textInputWrapper = shallow(<TextInput />);
textInputWrapper.find(".text-input")
.simulate("change", {
currentTarget: {
value: "Hello, world!",
},
});
textInputWrapper.update();
expect(textInputWrapper.find(".text-input").props().value)
.toBe("Hello, world!");
done();
});
I also used mount instead shallow and got same results...
Then I used actualInputValue and onChange handler :(
It's my questions: how to get actual value from textInputWrapper.find(".text-input") ?
Thank you so much!!!

I think the main issue you're having is within your expect statement (below)
expect(textInputWrapper.find(".text-input").props().value)
.toBe("Hello, world!");
You're running .props() on an array instead of a single node, assuming you know there will be only one ".text-input" replace it with the below.
expect(textInputWrapper.find(".text-input").at(0).props().value)
.toBe("Hello, world!");
You could also use .prop("value") instead of .props().value although that's more personal preference.
Also you don't need to use the done callback for this test, that's only for async functions such as MockApi calls

Related

Any issues using constructor functions as custom React Hooks?

I'm playing around with the use of constructor functions in a React application. However, I have never seen anyone discuss this usage before, so I'm wondering if there's a problem with it that I'm not aware of.
I have a constructor function called NameSection that is serving like a React hook. It has its own state (first and last name of a person) that it manages. I use it in my PersonalInfo component to render one of two things: either the current first/last name or inputs for the two fields so that they may be edited.
One reason I'm using a constructor function is because I'm considering turning it into an interface that can be inherited from. Then, I can later implement a PasswordSection, EmailSection, etc that need to follow a similar structure.
From my tests so far, I haven't seen any issues with using a constructor function in this particular context. But I'd like to hear others' thoughts on this. Are there any problems with this sort of implementation in the world of React?
In PersonalInfo.js
const PersonalInfo = ({info}) => {
const nameSection = new NameSection({firstName: info.firstName, lastName: info.lastName});
return (
<SettingsChild heading={"Name"} onSaveAttempt={nameSection.nameExitEdit} onEdit={nameSection.nameEnterEdit} disableSave={nameSection.isNameSectionError()}>
{nameSection.renderNameSection()}
</SettingsChild>
)
}
export default PersonalInfo;
In NameSection.js
function NameSection({firstName, lastName}) {
const initialNameSection = {
editMode: false,
firstName: {input: firstName, error: ""},
lastName: {input: lastName, error: ""}
}
const [ data, setData ] = useState(initialNameSection);
const handleNameChange = (event) => {
const key = event.target.name;
const value = event.target.value;
setData(prev => ({
...prev,
[key]: {input: value, error: ""}
}))
}
this.isNameSectionError = () => (!data.firstName.input || !data.lastName.input);
this.nameEnterEdit = () => {
setData(prev => ({
...prev,
editMode: true
}))
}
this.nameExitEdit = () => {
if (this.isNameSectionError()) {
if (!data.firstName.input) {
setData(prev => ({
...prev,
firstName: {...prev.firstName, error: inputErrors.requiredField}
}))
}
if (!data.lastName.input) {
setData(prev => ({
...prev,
lastName: {...prev.lastName, error: inputErrors.requiredField}
}))
}
}
else {
setData(prev => ({
...prev,
editMode: false
}))
}
}
this.renderNameSection = () => {
return (
data.editMode ?
<React.Fragment>
<TextBox label={"First name"} name={"firstName"} value={data.firstName.input} onChange={handleNameChange} errorMessage={data.firstName.error}/>
<TextBox label={"Last name"} name={"lastName"} value={data.lastName.input} onChange={handleNameChange} errorMessage={data.lastName.error}/>
</React.Fragment>
:
<p>{`${data.firstName.input} ${data.lastName.input}`}</p>
)
}
}
export default NameSection;

Why is onInput not a function? A question about useEffect()

I am pretty new to react and hooks, and I'm struggling with useEffect(). I've watched all the vids and read all the docs and still can't quite wrap my head around the error I'm getting. ("onInput is not a function" when my New Article route loads). onInput points to a callback function in my form-hook.js. Why isn't it registering?
In my input.js component:
import React, { useReducer, useEffect } from 'react';
import { validate } from '../../util/validators';
import './Input.css';
const inputReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
return {
...state,
value: action.val,
isValid: validate(action.val, action.validators)
};
case 'TOUCH': {
return {
...state,
isTouched: true
}
}
default:
return state;
}
};
const Input = props => {
const [inputState, dispatch] = useReducer(inputReducer, {
value: props.initialValue || '',
isTouched: false,
isValid: props.initialValid || false
});
const { id, onInput } = props;
const { value, isValid } = inputState;
useEffect(() => {
console.log(id);
onInput(id, value, isValid)
}, [id, value, isValid, onInput]);
const changeHandler = event => {
dispatch({
type: 'CHANGE',
val: event.target.value,
validators: props.validators
});
};
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
//if statement to handle if you are updating an article and touch the category.... but it's valid
const element =
props.element === 'input' ? (
<input
id={props.id}
type={props.type}
placeholder={props.placeholder}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
) : (
<textarea
id={props.id}
rows={props.rows || 3}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
);
return (
<div
className={`form-control ${!inputState.isValid && inputState.isTouched &&
'form-control--invalid'}`}
>
<label htmlFor={props.id}>{props.label}</label>
{element}
{!inputState.isValid && inputState.isTouched && <p>{props.errorText}</p>}
</div>
);
};
export default Input;
useEffect(() => {onInput points to the onInput prop in NewArticle.js component where users can enter a new article.
import Input from '../../shared/components/FormElements/Input';
import { useForm } from '../../shared/hooks/form-hook';
const NewArticle = () => {
const [formState, inputHandler] = useForm({
title: {
value: '',
isValid: false
}
}, false );
return (
<Input
id="title"
element="input"
type="text"
label="Title"
onInput={inputHandler}
/> );
};
export default NewArticle;
...and then in my form-hook.js inputHandler is a callback. So, onInput points to a callback function through a prop. It was working, registering onInput as a function and then, all of a sudden it was throwing an error. What gives?
import { useCallback, useReducer } from 'react';
const formReducer = (state, action) => {
switch (action.type) {
case 'INPUT_CHANGE':
let formIsValid = true;
for (const inputId in state.inputs) {
if (!state.inputs[inputId]) {
continue;
}
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: { value: action.value, isValid: action.isValid }
},
isValid: formIsValid
};
case 'SET_DATA':
return {
inputs: action.inputs,
isValid: action.formIsValid
};
default:
return state;
}
};
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity
});
const inputHandler = useCallback((id, value, isValid) => {
dispatch({
type: 'INPUT_CHANGE',
value: value,
isValid: isValid,
inputId: id
});
}, []);
const setFormData = useCallback((inputData, formValidity) => {
dispatch({
type: 'SET_DATA',
inputs: inputData,
formIsValid: formValidity
});
}, []);
return [formState, inputHandler, setFormData];
};
Thanks, ya'll.
I can give you some advice on how to restructure your code. This will ultimately solve your problem.
Maintain a single source of truth
The current state of your UI should be stored in a single location.
If the state is shared by multiple components, your best options are to use a reducer passed down by the Context API (redux), or pass down the container component's state as props to the Input component (your current strategy).
This means you should remove the Input component's inputReducer.
The onInput prop should update state in the container component, and then pass down a new inputValue to the Input component.
The DOM input element should call onInput directly instead of as a side effect.
Remove the useEffect call.
Separation of Concerns
Actions should be defined separately from the hook. Traditionally, actions are a function that returns an object which is passed to dispatch.
I am fairly certain that the useCallback calls here are hurting performance more than helping. For example inputHandler can be restructured like so:
const inputChange = (inputId, value, isValid) => ({
type: 'INPUT_CHANGE',
value,
isValid,
inputId
})
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity,
})
const inputHandler = (id, value, isValid) => dispatch(
inputChange(id, value, isValid)
)
}
Learn how to use debugger or breakpoints in the browser. You would quickly be able to diagnose your issue if you put a breakpoint inside your useEffect call.

Test prevProps in componentDidUpdate in React via Jest

Having following code:
componentDidUpdate(prevProps: JsonInputProps) {
if (prevProps.value !== this.props.value) {
this.validateJsonSchema(this.props.value || '');
}
}
and test code:
it('componentDidUpdate should mount', () => {
const onChange = jest.fn();
const event = { target: { value: 'simulate-event' } };
const wrapper = enzyme
.shallow(<JsonInput onChange={onChange} onValueChange={mockOnValueChange}/>)
.simulate('change', event);
wrapper.setProps({ value: 'example' });
expect(onChange).toBeCalled;
});
and test coverage:
I got 'else path not taken' and I do NOT want to ignore the else path but dunno how to change props. Any idea?
To cover else case you can pass in same prop value along with possibly something else along with it so that it covers else case upon update.
it('componentDidUpdate should mount', () => {
const onChange = jest.fn();
const event = { target: { value: 'simulate-event' } };
const wrapper = enzyme
.shallow(<JsonInput onChange={onChange} onValueChange={mockOnValueChange}/>)
.simulate('change', event);
wrapper.setProps({ foo: 'something' });
expect(onChange).toBeCalled;
});

Enzyme simulate not changing textarea

Im tryng a simple test to verify if the user can enter text on a text area, but for some reason the simulate method doesnt work..
CommentBox component
class CommentBox extends Component {
state = { comment: "" };
handleChange = event => {
this.setState({ comment: event.value });
};
handleSubmit = e => {
e.preventDefault();
this.setState({ comment: "" });
};
render() {
return (
<form>
<h4>
Add a CommentBox
<textarea onChange={this.handleChange} value={this.state.comment} />
<div>
<button onSubmit={this.handleSubmit}>Submit Comment</button>
</div>
</h4>
</form>
);
}
}
The test
export default CommentBox;
let wrapped;
beforeEach(() => {
wrapped = mount(<CommentBox />);
});
afterEach(() => {
wrapped.unmount();
});
it('has a text area and a button', () => {
expect(wrapped.find('textarea').length).toEqual(1);
expect(wrapped.find('button').length).toEqual(1);
});
it('has a text area that users can type in' ,() => {
const textarea = wrapped.find('textarea');
textarea.simulate('change', {
target: { ,
value: 'testing' }
});
wrapped.update();
expect(textarea.prop('value')).toEqual('testing');
});
error :
● has a text area that users can type in
expect(received).toEqual(expected) // deep equality
Expected: "testing"
Received: ""
Issue #1
handleChange = event => {
this.setState({ comment: event.value });
};
Actually value is event.target.value. But I believe better make destructuring right in arguments:
handleChange = ({ target: { value }}) => {
this.setState({ comment: value });
};
Issue#2:
after rerendering you have to run .find('textarea') again instead of relying on previous value; also you don't ever need wrapped.update()
So
it('has a text area that users can type in' ,() => {
wrapped.find('textarea').simulate('change', {
target: { value: 'testing' }
});
expect(wrapped.find('textarea').props().value).toEqual('testing');
});
works fine.

Jest MockImplementation of react component

I am testing a react component which renders another component which calls an endpoint and returns some data and is displayed, i want to know how i can mock the component that calls the endpoint and return dummy data for each test
This is the component i am testing
class MetaSelect extends React.Component {
render() {
console.log('metaselect render', MetadataValues);
return (
<MetadataValues type={this.props.type}>
{({ items, isLoading }) => (
<>
{isLoading ? (
<Icon variant="loadingSpinner" size={36} />
) : (
<Select {...this.props} items={items} placeholder="Please select a value" />
)}
</>
)}
</MetadataValues>
);
}
}
MetaSelect.propTypes = {
type: PropTypes.string.isRequired
};
I want to mock the MetadataValues in my tests, this is the metadataValues.js file
class MetadataValues extends React.Component {
state = {
items: [],
isLoading: true
};
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const items = await query(`....`);
this.setState({ items, isLoading: false });
};
render() {
return this.props.children({ items: this.state.items, isLoading: this.state.isLoading });
}
}
MetadataValues.propTypes = {
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired
};
This is my metaSelect.test.js file
jest.mock('../MetadataValues/MetadataValues');
describe.only('MetaSelect component', () => {
fit('Should display spinner when data isnt yet recieved', async () => {
MetadataValues.mockImplementation( ()=> { <div>Mock</div>});
const wrapper = mount(<MetaSelect type="EmployStatus"/>);
expect( wrapper.find('Icon').exists() ).toBeTruthy();
});
});
Im guessing i need to add something in the MetadataValues.mockImplementation( )
but im not sure what i should add to mock the component correctly
If you only need one version of the mock in your test this should be enough:
jest.mock('../MetadataValues/MetadataValues', ()=> ()=> <div>Mock</div>);
If you need to different mock behaviour you need to mock it like this
import MetadataValues from '../MetadataValues/MetadataValues'
jest.mock('../MetadataValues/MetadataValues', ()=> jest.fn());
it('does something', ()={
MetadataValues.mockImplementation( ()=> { <div>Mock1</div>});
})
it('does something else', ()={
MetadataValues.mockImplementation( ()=> { <div>Mock2</div>});
})
what about using shallow() instead of mount()?
const mockedItems = [{.....}, {...}, ...];
it('shows spinner if data is loading', () => {
const wrapper = shallow(<MetaSelect type={...} /*other props*/ />);
const valuesChildren = wrapper.find(MetadataValues).prop('children');
const renderResult = valuesChildren(mockedItems, true);
expect(renderResult.find(Icon)).toHaveLength(1);
expect(renderResult.find(Icon).props()).toEqual({
variant: "LoadingSpinner", // toMatchSnapshot() may be better here
size: 36,
});
});
This not only makes mocking in natural way but also has some benefits
it('passes type prop down to nested MetadataValues', () => {
const typeMocked = {}; // since it's shallow() incorrect `type` does not break anything
const wrapper = shallow(<MetaSelect type={typeMocked} >);
expect(wrapper.find(MetadataValues).prop('type')).toStrictEqual(typeMocked);
})

Categories