How to set the state of a react functional component using jest - javascript

I have a component , which has a form and modal. On click of submit, the modal pops up and on confirmation a call to back end is dispatched.
Initially the modal is hidden by using a state (displayModal).
I am trying to test the API call by finding the button inside display modal. But can't find it as it is not on DOM (display modal is false).
How Can I set the state on jest test.
const MyTypeComponent: FunctionComponent<MyType> = ({
props1,
props2,
ownProp1,
ownProp2
}) => {
//There are two use effects to do something
//set the modal state
const [displayModal, setdisplayModalOpen] = useState(false);
const updateStatusquantityService = () => {
//Call an API
};
const InventoryStatusquantityFormSubmitted = (form) => {
if (!form.isValid) {
return;
}
//If form valid, display the modal;
};
return (
<>
<Modal
isOpen={displayModal}
setIsOpen={setdisplayModalOpen}
id={"InventoryStatusTypeModal"}
>
//Usual Modal stuff and then button
<Button id={"statusquantityUpdateBtn"} variant="primary" label="Update" onClick={() => updateStatusquantityService()}/>
</Modal>
<Form>
//On form submit, call InventoryStatusquantityFormSubmitted() and display the modal
</Form>
</>
);
};
export default connect(
(state: RootState) => ({
//map states to props
}),
(dispatch: ThunkDispatch) => ({
//map props 1 and props 2
})
)(InventoryStatusquantity);
When I am trying to trigger a click even on modal button 'statusquantityUpdateBtn' by finding it as below, I am getting an empty value as modal is not visible due to it's value.
it('Should submit status types form11', () => {
const submitButtonOnModal = wrapper.find('#statusquantityUpdateBtn').
});
I am trying to update the state by using
wrapper.instance().setdisplayModalOpen(true)
But getting error wrapper.instance().setdisplayModalOpen is not a function.
I am mounting with simple mount command:
export const mountWithMemoryRouter = (element: JSX.Element) => {
return mount(<MemoryRouter>{element}</MemoryRouter>);
};
wrapper = mountWithMemoryRouter(
<Theme>
<Provider store={store}>
<MyTypeComponent
{...defaultProps}
ownProp1={null}
ownProp2={null}
/>
</Provider>
</Theme>
);

Those state hooks are scoped to the function, so nothing outside the function can access them. That's why you're getting "is not a function" errors. It's akin to
function x() {
const y = 0
}
x().y // Error
I don't see in your code anything that calls setdisplayModalOpen(true) in order to show the modal.
Assuming you provided only partial code (but that it's written on your computer), and there is some button or something that runs setdisplaymodalOpen(true), (I'm assuming there's a form submit button) then if I were needing to test this, I would instead use React Testing Library and have something like
import { render, screen, fireEvent, waitFor } from 'react-testing-library'
import MyComponent from './components/one-to-test'
test('does whatever', async () => {
render(<MyComponent/>)
const showModalBtn = screen.getByText('Text of Button You Click to Display Modal')
fireEvent.click(showModalBtn)
await waitFor(() => expect(screen.getByText('Update')).not.toBeNull())
// You are now assured the modal is visible and can continue with the rest of your test
})
In this test, you first instruct React Testing Library to render the component that can show/hide the modal (i.e., the form). (Assuming there's a button you click to display the modal), you get that button, and then you simulate a click of that button, and then your test waits for the modal to be visible (in this case, it waits until the "Update" button contained in the modal is visible).
Then you can continue with testing your modal (like clicking the Update button with another fireEvent.click(updateBtn).
If you want to mock out your API, then you could also add
jest.mock('./my/api/library', () => ({
whateverApiCall: jest.fn(() => whateverItShouldReturn)
})
Now when you click the form submit button, it will call your mocked API function that returns whatever you defined it to return, and assuming it doesn't throw/reject, your modal will display, and you continue as described above.

Related

React Hook "React.useEffect" is called in function "selectmenu" which is neither a React function component or a custom React Hook function

Problem: React Hook "React.useEffect" is called in function "selectmenu" which is neither a React function component or a custom React Hook function.
Target: I want to Mount Component('component DidMount/WillUnmount) (using useEffect()) only when I click on a button, rather than, mounting while the file (or whole Component) is being loaded.
Actual Goal: I want to select(or highlight) a file (custom ) on click. But when the user clicks outside the dimensions of the file (), then the selected file should get deselected (remove highlight).
export default function Academics() {
let [ ismenuselected, setmenuselection] = useState(0)
const selectmenu = () => {
console.log("Menu to Select")
React.useEffect(() => {
console.log('Component DidMount/WillUnmount')
return () => {
console.log('Component Unmounted')
}
}, [isfolderselected]);
}
return (
<div onClick={selectmenu}></div>
)
}
Note:
I've tried with the UPPER case of SelectFolder #56462812. Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I want to try something like this. But with a click of a button (useEffect() should invoke onClick event).
I think I got what you're trying to accomplish. First, you can't define a hook inside a function. What you can do is trigger the effect callback after at least one of its dependencies change.
useEffect(() => {
// run code whenever deps change
}, [deps])
Though for this particular problem (from what I understood from your description), I'd go something like this:
export default function Academics() {
let [currentOption, setCurrentOption] = useState()
function handleSelectOption(i) {
return () => setCurrentOption(i)
}
function clearSelectedOption() {
return setCurrentOption()
}
return (options.map((option, i) =>
<button
onClick={handleSelectOption(i)}
className={i === currentOption ? 'option option--highlight' : 'option'}
onBlur={clearSelectedOption}
></button>
))
}

Side effect function is not getting called in Cmd.run using redux-loop

I am working on a react redux application where in, on a button click I need to change my window location.
As of now, I am dispatching the button click action and trying to achieve the navigation in reducer using redux-loop.
Component js
class Component {
constructor() {
super()
}
render() {
return (
<button onClick={() => this.props.onButtonClick()}>Navigate</button>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
"onButtonClick": () => dispatch(handleClick())
};
}
Action js
export const handleClick = () => ({
type: NAVIGATE
});
Reducer js
export default (state = {}, action) => {
if (action.type === NAVIGATE) {
return loop(state, Cmd.run(navigateTo));
}
};
Effect js
export const navigateTo = () => {
window.location = "https://www.stackoverflow.com";
}
Apart from this action, I have lot many actions that involve side effect as well as state manipulation, hence redux-loop.
I have two questions:
Control is not going into navigateTo() on button click. What am I doing wrong?
I feel reducer is not a right place for it as we are not manipulating state here.
What would be the best place to put this piece of code when button click action is dispatched?
the code you have looks correct. Did you use the store enhancer when creating your redux store? Did you try setting a breakpoint in your reducer and verifying it gets called as you expect? https://redux-loop.js.org/docs/tutorial/Tutorial.html

Passing information from child component to parent

This is a follow up question to this question:
Call child method from parent
I am using React > 16.8 with function components and hooks.
I got a parent component which manages a component to display items and add a new item. The list of items is at the parent component. The way the items are added are by a "+" button which opens a new modal window (which is a component of its own), and inside there's a form that the user can insert the details of the new items.
const registerFormRef = useRef();
<Modal
isOpen={isFormOpen}
onCancel={() => setIsFormOpen(false)}
onSubmit={() => { registerFormRef.current.onSubmitForm(); setIsFormOpen(false) }}
titleText="Register Tenant">
<AddItem onAddItem={AddNewItem} ref={registerFormRef}></RegisterTenant>
</Modal>
The AddNewItem is a callback which adds the new item to the list. The modal has an "OK" button which serves as a submit button. It belongs to the parent modal component, not the AddItem child.
The method in the child component:
useImperativeHandle(ref, () => (
{
onSubmitForm()
{
setIsLoading(true);
const newItem = {
name: formSettings["name"].value,
description: formSettings["description"].value,
area: "",
category: ""
}
props.onAddItem(newItem);
setIsLoading(false);
}
}));
I had an issue of getting the information from the child component which holds the form to the parent component, since the submit button as I said, belongs to the modal, I had to somehow call the callback from inside the child form. I have used the accepted answer in the linked question above. It works, but the comment says it's not a good practice passing information like that. Is there another way of passing the information from the child form to the parent component?
The correct way is to store the form data in the parent i.e the component rendering the modal. To do that you could define a state and provide an onChange handler to it. Once you do that on any change in input the AddItem component must notify its parent by calling the onChange method
const App = () => {
const [data, setData] = useState();
const handleChange=(newData) => {
setData(newData);
}
const onSubmit = () => {
// use data to do whatever you want with the formData
console.log(data);
setIsFormOpen(false)
}
...
return (
<Modal
isOpen={isFormOpen}
onCancel={() => setIsFormOpen(false)}
onSubmit={onSubmit}
titleText="Register Tenant">
<AddItem onAddItem={AddNewItem} handleChange={handleChange} ref={registerFormRef}></RegisterTenant>
</Modal>
)
}

React form can be submitted twice despite conditional render

I have a React application coupled to Redux. There is a component rendering a form wrapper (a custom implementation of Formik), while the form inputs themselves are rendered by a child component.
(Not the exact code, but gets the point across.)
...
render() {
const {
config,
updateContactDetails,
errorMessages,
contactDetails,
previousFormValues,
isUpdating,
} = this.props;
const { apiBaseUrl, fetchTimeout, globalId } = config;
const initialValues = previousFormValues || getInitialContactDetailsValues(contactDetails);
if (isUpdating) return <Spinner />;
return (
<Form
initialValues={initialValues}
validate={(values) => validate(values, errorMessages)}
onSubmit={(values) => {
updateContactDetails(apiBaseUrl, globalId, values, fetchTimeout); // dispatch action
}}
>
<ContactDetailsForm content={content} />
</Form>
);
}
...
When you click the submit button in ContactDetailsForm, the value of isUpdating in the Redux store is set to true. As you can see above, that causes the the form to be replaced with a spinner component. However, it is somehow possible to submit the form twice by clicking the button twice.
How can this be? Could there be re-render happening before the one that replaces the form with the spinner? I know I can solve the problem by passing isUpdating into ContactDetailsForm and using it to disable the button, but I still want to illuminate the cause.
EDIT
The reducer looks something like this, in case it helps:
case UPDATE_CONTACT_DETAILS_START: {
return {
...state,
errorUpdatingContactMethods: {},
hasUpdatedContactDetails: false,
isUpdating: true,
contactDetailsValues: action.values,
};
}
You should instead set a disabled property on the button based on the isUpdating prop. It might be that it's just a race condition.

How to redirect/collapse an expanded edit form after hitting submit in React-Admin

I am currently trying to collapse the edit form from the list view in React-Admin after hitting the submit button. I want to be able to view those updated changed in the new list form after hitting submit as well.
Right now I have a class which handles when the form is submitted, and I am using window.location.replace('URL') within that class to try and redirect the page. The problem I am currently running into is that since I want the page to redirect to the same URL, It doesn't show the changes made and also doesn't collapse the edit form. When I use a different URL and go back to the list view it shows all the changes. So what I essentially want is to just reload the page (so everything is updated and the edit bar is collapsed) after the form is submitted. However, when I used window.location.reload(), it reloads the page immediately before saving any of the data from the edit form.
This is the code I currently have which handles submit:
const saveWithNote = (values, basePath, redirectTo) =>
crudCreate(
'usergrouprights',
{
...values,
GroupRights: CalcGroupPermissions(values).rights,
GroupDenials: CalcGroupPermissions(values).denials
},
basePath,
{
redirectTo
}
);
class SaveWithNoteButtonView extends Component {
handleClick = () => {
const { basePath, handleSubmit, redirect, saveWithNote } = this.props;
return handleSubmit(values => {
saveWithNote(values, basePath, redirect);
window.location.replace('/#/usergrouprights');
// window.location.reload();
});
};
render() {
const { handleSubmitWithRedirect, saveWithNote, ...props } = this.props;
return (
<SaveButton handleSubmitWithRedirect={this.handleClick} {...props} />
);
}
}
const SaveWithNoteButton = connect(
undefined,
{ saveWithNote }
)(SaveWithNoteButtonView);
export default SaveWithNoteButton;
In the list view, I have a custom toolbar which is the following:
const PostEditToolbar = props => (
<Toolbar {...props}>
<SaveWithRightsValuesButton />
</Toolbar>
);
When the code is run, It submits everything properly but I need to manually reload the page to view the changes and for the edit form to collapse as well. I am unsure if using the window.location.replace or reload call is the right idea as it is not working the way I would like it to at the moment.
Try adding
redirect="list"
to SaveButton:
<SaveButton
handleSubmitWithRedirect={this.handleClick}
redirect="list"
{...props}
/>

Categories