Testing a button with a method callback in a functional component - javascript

I am trying to test a react component using Enzyme. I am not able to test the click on the IconButton component and the function doesn't get called when i simulate a click.
This is how IconButton is defined on an external component.
var IconButton = function (props) {
return (React.createElement(IconButton$1, { color: 'default', onClick: props.onClick, disabled: props.disabled, size: props.size, onMouseDown: props.onMouseDown }, props.children));
};export{Button,IconButton};
This is how I am using it in my app.
import React, {useState, useEffect} from 'react';
import { Drawer } from '#material-ui/core';
import ExpandLessIcon from '#material-ui/icons/ExpandLess';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import { IconButton } from '#mycomponent/button';
export default function Component1 {
const classes = useStyles();
const [open, setOpen] = useState(true);
const handleClick = function (event) {
if (event) {
setOpen(!open);
}
else {
return;
}
};
return (
<Drawer>
<div className="classname1">
<IconButton onClick={(e) => handleClick(e)} className={classes.button, "iconBtn"}>
{open ? <ExpandLessIcon data-test="lessIcon" /> : <ExpandMoreIcon data-test="moreIcon" />}
</IconButton>
</div>
</Drawer>
);
}
Here is my test for simulating the click on the Icon Button. I also tried another way to check that the handleClick was getting called but it still fails.
const wrapper = shallow(<Component1 />);
it('Test the button click', () => {
expect(wrapper.containsMatchingElement(<ExpandMoreIcon />)).toBeTruthy()
const element = wrapper.find(".iconBtn")
const mockEvent = {target: {}};
element.simulate('click', mockEvent)
expect(wrapper.containsMatchingElement(<ExpandLessIcon />)).toBeTruthy()
})

Try changing this line:
const element = wrapper.find("button").at(0);
or you could find it by it's className from debug():
const element = wrapper.find(".MuiButtonBase-root MuiIconButton-root");
Notice that you'd simulate a click on an actual html button in such case.

Related

Reactjs update state after call component in functional component

I created a component called Alertify.js
import React, {useEffect, useState} from "react";
import { Alert } from 'reactstrap';
function Alertify(props){
const [show, setShow] = useState(props.show);
useEffect(
() => {
let timer = setTimeout(() => setShow(false), 3000);
return () => {
clearTimeout(timer);
};
},
[]
);
return (
<Alert color={props.color} className={show ? 'float active' : 'float'}>{props.text}</Alert>
)
}
export default Alertify;
And used in index.js
import Alertify from "../Component/Alertify";
const [showAlert, setShowAlert] = useState(false);
return...
<Alertify text={'hello world'} color={'danger'} show={showAlert}/>
And it will show this alert after a condition is true:
if(condition){
setShowAlert(true)
}
But something is wrong and not show alert on condition, and I'm newbie to reactjs, any idea how to fix this?
All I want is show alert after condition is true, then hide after 3 seconds.
Also it show if I remove useEffect but before condition true, and also not hiding.
Try the following
const [show, setShow] = useState(false);
useEffect(() => {
if (props.show) {
setShow(true)
}
},[props.show])
You can leave your existing useEffect to clear after 3 seconds as is.
EDIT:
Here's a modified approach, your Alertify component looks like so
import React, { useEffect, useState } from "react";
import { Alert } from "reactstrap";
function Alertify(props: {
show: any;
color: string;
text: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal;
setShowAlert: (value: boolean) => void;
}) {
const [show, setShow] = useState(false);
useEffect(() => {
let timer = setTimeout(() => {
return setShow(false);
}, 3000);
return () => {
clearTimeout(timer);
};
});
useEffect(() => {
if (props.show) {
setShow(true);
props.setShowAlert(false);
}
}, [props.show, props.setShowAlert]);
if (show) {
return (
<Alert color={props.color} className={show ? "float active" : "float"}>
{props.text}
</Alert>
);
}
return null;
}
export default Alertify;
Your calling component then looks like so
import "./styles.css";
import Alertify from "./Alertify";
import { useState } from "react";
export default function App() {
const [showAlert, setShowAlert] = useState(false);
return (
<>
<div className="App">
<Alertify
text={"hello world"}
color={"danger"}
show={showAlert}
setShowAlert={setShowAlert}
/>
</div>
<button onClick={() => setShowAlert(true)}>show alert</button>
</>
);
}
Here's the codesandbox link https://codesandbox.io/s/alertify-stackoverflow-x096t
Try this code. You can control rendering Alertify component in index.js. If showAlert value is true, React render Alertify component. When setTimeout executed, showAlert value will be false which means React unmount Alertify component. This is like show and hide effect as what you need.
// index.js
import Alertify from "../Component/Alertify";
const [showAlert, setShowAlert] = useState(false);
if (condition( {
setShowAlert(true); // This makes Alertify component mount immediatly.
setTimeout(setShowAlert(false),3000); // This makes Alertify component unmount in 3,000ms.
}
return...
{showAlert && <Alertify text={'hello world'} color={'danger'}/>}
Therefore, you don't need to use useEffect to make this component hide.
// Alertify.js
import React, {useEffect, useState} from "react";
import { Alert } from 'reactstrap';
function Alertify(props){
return (
<Alert color={props.color} className={'float active'}>{props.text
</Alert>
)
};
export default Alertify;

test if a modal disappears after clicking Delete

Beginner with react testing,
I am using jest and react testing library, here I have a component 'A' which is a modal, I'm trying to implement a test to it, when the user clicks a button 'Delete Link' then this modal should disappear(function onDelete). As you can see I'm clicking the button using FireEvent.click() so after it when changing toHaveBeenCalledTimes(0) from 0 to 1, I'm getting Expected number of calls: 1 Received number of calls: 0, shouldn't be expected and received both be 1?
The end component(modal) should not be visible to the user after clicking Delete Link.
Can someone enlighten me with this?
Any suggestions/help is appreciated.
English is not my mother language so there might be mistakes.
my code:
import React from "react";
import { render, screen, cleanup, fireEvent } from "#testing-
library/react";
import { LinkForm } from "../forms/LinkForm";
import { Provider } from "react-redux";
import { store } from "../../redux/store";
import "#testing-library/jest-dom/extend-expect";
describe("Testing component", () => {
const onClickCallback = jest.fn();
test("Testing if link is deleted when button 'Delete Link' is clicked", () => {
const mockDelete = jest.fn();
const props = {
onDelete: mockDelete,
};
render(
<Provider store={store}>
<LinkForm
classes={{ button_basic: "", formControl: "" }}
key={""}
onSubmit={onClickCallback}
onCancel={onClickCallback}
// onClick={onClickCallback()}
{...props}
/>
</Provider>
);
const component = screen.getByTestId("LinkForm");
const deleteLinkButton = screen.getByRole("button", {
name: /Delete Link/i,
});
expect(deleteLinkButton).toBeVisible();
fireEvent.click(deleteLinkButton);
expect(mockDelete).toHaveBeenCalledTimes(0);
expect(component).toBeVisible();
});
});
import React, { useEffect, useState } from "react";
import { connect, RootStateOrAny, useDispatch, useSelector } from "react-redux";
import { Trans } from "react-i18next";
import { editLink, changeLink, removeLink } from "../../redux/actions";
import {Button} from "#material-ui/core/";
import { Done, Delete } from "#material-ui/icons";
interface AFormProps {
key: string;
onSubmit: () => void;
onCancel: () => void;
onClick?: () => void;
classes: {
button_basic: string;
formControl: string;
};
}
const A: React.FC<AFormProps> = (props) => {
const dispatch = useDispatch();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
dispatch(editLink(linkSettings));
externalOnSubmit();
};
const onDelete = () => {
// Delete selected link from graph
dispatch(removeLink(currentLink.id));
dispatch(changeLink(""));
};
const disabled = currentLink ? false : true;
return (
<form onSubmit={handleSubmit} data-testid="LinkForm">
<Button
id="delete"
type="button"
onClick={onDelete}
disabled={disabled}
variant="outlined"
color="secondary"
className={classes.button_basic}
startIcon={<Delete />}
>
<Trans i18nKey="form.linkForm.delete">Delete Link</Trans>
</Button>
</form>
);
};
export const LinkForm = connect(null, null)(A);
In the test, you are passing a mock function to the onDelete prop of the form but this is not defined in AFormProps nor is an onDelete prop being consumed in the component. The onDelete function is created within the component scope and set as the onClick for the button. The mock function will never be used in this case.

React elements are removed on state update

I'm supposed to have a modal appear with an image in it. There are next and previous buttons which controls which image you are currently viewing. The modal is rendered in a portal. That in itself is working correctly. However, when I add children, and those childrens are updated, the modal only (not the portal) gets removed from the flow. In the React DevTools, the "isOpen" state of the modal is still set to true. I am using React 17.0.2 with NextJS 12.0.4 and Styled Components 5.3.3.
I have tried:
memoizing my components (as you can see there are some remnants of those trials) but this did not work
extracting the state of the modal to the parent and passing it as props and it didn't work either
I know there must be something wrong that I'm doing here so if you could help me find it that would be much appreciated!
Here is the controller where the modal is rendered:
import { FC, MouseEventHandler, useEffect, useState } from "react";
import { Photo } from "services/Images/Images.interfaces";
import { useGetNextPhoto, useGetPhotos, useGetPreviousPhoto } from "state";
import SlideshowContextProvider from "./Context/SlideshowContext";
import SlideShowModal from "./SlideShowModal";
const SlideshowController: FC = () => {
const photos = useGetPhotos();
const [currentlyViewedPhoto, setCurrentlyViewedPhoto] = useState<Photo | null>(null);
const nextPhoto = useGetNextPhoto(currentlyViewedPhoto?.id);
const previousPhoto = useGetPreviousPhoto(currentlyViewedPhoto?.id);
const onPreviousRequest: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(previousPhoto);
};
const onNextRequest: MouseEventHandler<HTMLButtonElement> = async (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(nextPhoto);
};
useEffect(() => {
setCurrentlyViewedPhoto(photos[0]);
}, [photos]);
return (
<SlideshowContextProvider
currentlyViewing={currentlyViewedPhoto}
onNextSlideRequest={onNextRequest}
onPreviousSlideRequest={onPreviousRequest}
>
<SlideShowModal />
</SlideshowContextProvider>
);
};
export default SlideshowController;
The SlideshowModal:
import { Modal } from "components";
import { FC } from "react";
import SlideshowControlBar from "./SlideshowControlBar";
import SlideshowImage from "./SlideshowImage";
const SlideShowModal: FC = () => {
return (
<Modal uniqueKey="slideshow">
<SlideshowImage />
<SlideshowControlBar />
</Modal>
);
};
export default SlideShowModal;
The modal in itself:
import Portal from "components/Portal/Portal";
import { FC, useEffect, useMemo, useState } from "react";
import { useRegisterModal } from "state";
import styled from "styled-components";
import useWindowScrollLock from "./hook/UseWindowScrollLock";
interface Props {
uniqueKey: string;
isBackgroundOpaque?: boolean;
}
... Styled elements
const Modal: FC<Props> = ({ uniqueKey, isBackgroundOpaque = true, children }) => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const register = useRegisterModal(uniqueKey);
const isModalOpen = useMemo(() => isOpen, [isOpen]);
useEffect(() => {
register({ open, close });
}, [register]);
useWindowScrollLock(isModalOpen);
return isModalOpen ? (
<Portal>
<Container>
<InnerModal>
<Close onClick={close}>X</Close>
{children}
</InnerModal>
</Container>
<Background onClick={close} opaque={isBackgroundOpaque} />
</Portal>
) : null;
};
export default Modal;

Can we access functions outside of stateless component in react?

Trying to invoke modal function from outside, but not possible. Achieved with class component using static method and properties, but this will not work there. Observers are giving some issues, finally landing to stateless component. How best way we can make this work?
//Inside
import Modal from './modal';
// Not working
<Button onClick={Modal.showModal}
<Modal />
//Outside
export const Modal = () => {
const [visible, setVisible] = useState(false);
const showModal = () => {
setVisible(true);
}
return(
<Dialog visible={visible}>Hello Test</h1>
)
}
You can declare setVisible in parent component:
const [visible, setVisible] = useState(true);
Then pass the set function to Modal:
<Modal visible={visible} setVisible={setVisible} />
Set visible from outside:
<button onClick={() => setVisible(!visible)}>Click</button>
Sample: https://codesandbox.io/s/competent-bassi-x3dqd?file=/src/App.js:267-325
You will need to "lift the state up" to a component that will pass the state and the state handlers to his children, or use some global state with redux or context.
Example:
<App>:
import Modal from "./modal";
import { useState } from "react";
const [visible, setVisible] = useState(false);
const showModal = () => {
setVisible(true);
};
return (
<>
<Button onClick={showModal}> Hide Modal </Button>
<Modal visible={visible} />
</>
);
<Modal>:
export default const Modal = ({ visible }) => {
return (
<Dialog visible={visible}>
<h1>Hello Test</h1>
</Dialog>
);
};
Also, by only using the export keyword, you can import only single export from a module, therefore you can't use the default import syntax import Modal from "..."
and you will be required to import as such - import { Modal } from '...'
I managed to solve usingEffect here, but not sure if this is perfect for my implementation. I am trying to update the state for already mounted component.
useEffect(() => {
if (props.visible) {
showModal();
}
}, [props.visible]);
And handled resetState with a callback prop

Why won't one of my components hide onclick using hooks?

For the life of me I can't figure out why the Spin Now won't hide upon clicking it. It rightfully shows Claim Now upon clicking Spin Now but once Claim Now shows, I want the Spin Now to hide. I'm using hooks, what am I doing wrong?
import React, { useState } from 'react';
import SpinNowButton from '../../components/SpinNowButton/SpinNowButton';
import ClaimNowButton from '../../components/ClaimNowButton/ClaimNowButton';
import './Buttons.css';
const Buttons = () => {
const [showSpin, setShowSpin] = useState(false);
const [showClaim, setShowClaim] = useState(false);
return(
<div className="both-buttons">
<SpinNowButton onClick={() => setShowClaim(true)}/>
{showClaim ? <ClaimNowButton/> : null}
{showSpin ? <SpinNowButton/> : null}
</div>
);
};
export default Buttons;
So you only need one piece of state to accomplish this, and you set it to the opposite of what the value was previously. Then the ternary components below will render one component if true, and the other if false.
Edit - I think this is closer to what you're looking for:
import React, { useState } from 'react';
import SpinNowButton from '../../components/SpinNowButton/SpinNowButton';
import ClaimNowButton from '../../components/ClaimNowButton/ClaimNowButton';
import './Buttons.css';
const Buttons = () => {
const [showClaim, setShowClaim] = useState(false);
const handleCLick = () => {setShowClaim(!showClaim)}
return(
<div className="both-buttons">
{showClaim ? null : <SpinNowButton onClick{() => handleClick()}/>}
{showClaim ? <ClaimNowButton onClick{() => handleClick()}/> : null}
</div>
);
};
export default Buttons;
An implementation which is flexible in adding more types such as claim/spin:
import React, { useState } from 'react';
import SpinNowButton from '../../components/SpinNowButton/SpinNowButton';
import ClaimNowButton from '../../components/ClaimNowButton/ClaimNowButton';
import './Buttons.css';
const Buttons = () => {
const [shownItems, setShownItems] = useState(['spin']);
return(
<div className="both-buttons">
{shownItems.contains('claim') && <ClaimNowButton onClick={() => setShownItems('spin')}/>}
{shownItems.contains('spin') && <SpinNowButton onClick={() => setShownItems('claim')}/>}
</div>
);
};
export default Buttons;

Categories

Resources