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;
});
Related
i was doing a todo list app on React, and, tring to handle the changes i got the following error:
Uncaught TypeError: prevTodos is not iterable
My handle function:
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
Full Code:
import React, { useState, useRef, useEffect } from "react";
import TodoList from "./TodoList";
import { v4 } from "uuid";
const LOCAL_STORAGE_KEY = 'todoApp.todos';
function App() {
const [todos, setTodos] = useState([]);
const todoNameRef = useRef();
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
return (
<>
<TodoList todos={todos} />
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Complete</button>
<div>0 left to do</div>
</>
);
}
export default App;
This lines are the problem
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
you should parse the value before setting and there is no need for that setTodos() without value, because of that you would later get "undefined" is not a valid JSON:
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(JSON.parse(storedTodos));
}, []);
I haven't ran the code or tested anything but I think it's just the way your calling setTodos. Instead of passing in an anonymous function I would define the new todo list first, then use it to set the state. Try something like this.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
const newTodos = [...prevTodos, { id: v4(), name: name, complete: false }];
setTodos(newTodos);
todoNameRef.current.value = null;
}
You could probably get away with this too.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos([...prevTodos, { id: v4(), name: name, complete: false }]);
todoNameRef.current.value = null;
}
Hopefully that helps.
I know this is an over asked question regarding useEffect and useState infinite loops. I have read almost every question about it and also searched over the internet in order to try to fix it before posting here. The most recent article that I read was this one.
Before I read the article my useState function inside useEffect was without the useState's callback, so it was what I thought which was causing the problem. But after moving it from this:
setNotifications({
...notifications,
[type]: {
...notifications[type],
active: portfolioSettings[type],
payload: {
...notifications[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
});
to this:
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
payload: {
...currState[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
}));
Unfortunately the infinite loop persists. Here are all useEffect functions (the last one is the one causing the infinite looping) that I'm using in this component.
// Set default selected portfolio to the active portfolio
useEffect(() => {
let mounted = true;
const setPortfolioData = () => {
if (activePortfolio) {
const portfolioId = activePortfolio.id;
const portfolioName = activePortfolio.name;
setSelectedPortfolio({
portfolioId,
portfolioName,
});
}
};
if (mounted) setPortfolioData();
return () => {
mounted = false;
};
}, [activePortfolio]);
// Set all the categories if no category is set
useEffect(() => {
let mounted = true;
if (mounted && allCvmCategories) {
const allCvmCategoriesNames = allCvmCategories.map((c) => c);
setNotifications((currState) => ({
...currState,
isCvmNotificationEnabled: {
...currState.isCvmNotificationEnabled,
payload: {
...currState.isCvmNotificationEnabled.payload,
cvmCategories: allCvmCategoriesNames,
},
},
}));
setIsCvmCategoriesReady(true);
}
return () => {
mounted = false;
};
}, [allCvmCategories]);
// THE ONE WHICH IS CAUSING INFINITE LOOPING
// THE notificationsTypes AND notificationsInitialState
// ARE DECLARED IN THE ROOT OF THE JSX FILE
// OUT OF THE FUNCTIONAL COMPONENT
useEffect(() => {
let mounted = true;
if (
mounted &&
isCvmCategoriesReady &&
allPortfolios &&
selectedPortfolio.portfolioId
) {
const { portfolioId } = selectedPortfolio;
const portfolioSettings = allPortfolios[portfolioId].settings;
if (portfolioSettings) {
notificationsTypes.forEach((type) => {
if (Object.prototype.hasOwnProperty.call(portfolioSettings, type)) {
const { payloadKeys } = notificationsInitialState[type];
if (payloadKeys.length > 0) {
payloadKeys.forEach((pKey) => {
if (
Object.prototype.hasOwnProperty.call(portfolioSettings, pKey)
) {
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
payload: {
...currState[type].payload,
[pKey]: portfolioSettings[pKey],
},
},
}));
}
});
} else {
setNotifications((currState) => ({
...currState,
[type]: {
...currState[type],
active: portfolioSettings[type],
},
}));
}
}
});
}
}
return () => {
mounted = false;
};
}, [allPortfolios, isCvmCategoriesReady, selectedPortfolio]);
Thanks for the responses. I have found the problem, it was with the hook that I was using (SWR) for fetching. Making the allPortfolios change all the time. I have fixed it wrapping into a new custom hook.
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.
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
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);
})