How to write a test in Jest for onClick function? - javascript

I have a React component and I would like to write a test to see if onClick renders properly when clicked title div.
I tried to do this with userEvent, but it is not working, not sure where there is a mistake.
Here is a piece of code:
const DropDown = ({ children, text, color, activeColor }) => {
const [isActive, setIsActive] = useState(false)
return (
<Styled.DropDown color={color}>
<Styled.Title
variant="h5"
as="div"
onClick={() => setIsActive(!isActive)}
color={isActive ? activeColor : color}
>
{text}
<Styled.Icon isActive={isActive}>
<Icon name="arrowUp" />
</Styled.Icon>
</Styled.Title>
{isActive && <Styled.Content>{children}</Styled.Content>}
</Styled.DropDown>
)
}
and the test I tried to write:
it(`fires onClick callback when accordion is clicked`, () => {
const onClickMock = jest.fn()
render(<DropDown onClick={onClickMock} />)
const link = screen.getByRole("link")
act(() => {
userEvent.click(link)
})
expect(onClickMock).toBeCalledTimes(1)
})

Related

Unable to use onMouseEnter and OnMouseLeave on a component

I would like on hover of my ActionOverflow component the a message 'more options' displays. I'm unable to add onMouseEnter and OnMouseLeave to the component. I've tried wrapping it in a parent div and it displays the message but the ActionOverflow which is displaying an Ellipsis icon moves very far left in the div when it's hovered over. How can I achieve this and not have the ellipsis move?
const menuItems: string[] = ['hghhg']
const EllipsisMenu = () => {
const [active, setActive] = useState(false);
const [hover, setHover] = useState(false);
const handleClick = () => {
setActive(!active);
};
const handleClickAway = () => {
setActive(false);
};
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
return (
<>
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{hover ? 'more options' : null}
{menuItems.length > 0 ?
<ActionOverflow active={active} onClick={handleClick} onClickAway={handleClickAway} >
{menuItems.map((item: string, index: number) => (
<div key={index}>{item}</div>
))}
</ActionOverflow>
:
null
}
</div>
</>
);
};
export default EllipsisMenu

ReactJS onclick toggle class event multiple times

How can I use this onclick toggle class event multiple times?
const [isActive, setActive] = React.useState(false);
<button
onClick={() => setActive(!isActive)}
className={`toggle ${isActive ? "active" : ""}`}
>
Categories
</button>
Here is the issue in Sandbox: https://codesandbox.io/s/agitated-pond-tewk9h?file=/src/App.js
When click on any button it works for all others.
You should create a component for your button that contains the useState.
Else, the useState is global for all the components that use it.
Example:
https://codesandbox.io/s/withered-feather-vj8owx?file=/src/App.js
const MyButtonComponent = ({myText}) => {
const [isActive, setActive] = React.useState(false);
return (
<button
onClick={() => setActive(!isActive)}
className={`toggle ${isActive ? "active" : ""}`}
>
{myText}
</button>
);
};
export default function App() {
return (
<>
<MyButtonComponent myText="Something" />
<MyButtonComponent myText="Something else"/>
<MyButtonComponent myText="Else something"/>
</>
);
}
This is for reference:
Edited answer from Magofoco
Passing button text & ID:
const MyButtonComponent = ({ myText, myId }) => {
const [isActive, setActive] = React.useState(false);
return (
<Button
onClick={() => setActive(!isActive)}
className={`toggle ${isActive ? "active" : ""}`}
id={myId}
>
{myText}
</Button>
);
};
export default function App() {
return (
<MyButtonComponent myText="something" myId="tab01" />
);
}

Only allow one React Accordion to be expanded at once

I have an array of data that will be used to create accordions, I'd like to make it so that only one of them can be expanded at once (i.e, if the user expands accordion #1 and then #2, #1 will un-expand)
I have this code:
const MyAccordion = props => {
const [expanded, setExpanded] = React.useState()
const handleChange = panel => (_, isExpanded) => {setExpanded(isExpanded ? panel : false)}
const classes = styles //?
let accordionInfo = createAccordionInfo(props.propthing);
return (
<Accordion
key={accordionInfo.uid}
onChange={handleChange(accordionInfo.uid)}
expanded={expanded === accordionInfo.uid}
TransitionProps={{unmountOnExit: true}}
className={classes.accordion}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`${accordionInfo.uid}-content`} id={`${accordionInfo.uid}-header`}>
<Typography>Accordion Summary</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Accordion Details</Typography>
</AccordionDetails>
</Accordion>
)
}
const MyAccordions = props => {
const [expanded, setExpanded] = React.useState()
const handleChange = panel => (_, isExpanded) => {setExpanded(isExpanded ? panel : false)}
return (
<div className={styles.root}>
{accordions.map(accordion => (
<MyAccordion onChange={handleChange} propthing={accordion} />
))}
</div>
)
}
I'm quite new to React so I suspect I've made a mistake with the states. Any help / tips would be appreciated! Thank you
It looks like you tried putting state and handlers in both the parent MyAccordions and the children MyAccordion components. If you want only one accordion open at-a-time then I suggest placing the state in the parent component so it can manage what is open/expanded. Use the children accordion ids as the basis of determining which should expand.
Parent
const MyAccordions = props => {
const [expanded, setExpanded] = React.useState(null);
const handleChange = id => (_, isExpanded) => {
// if expanded, set id to open/expand, close it otherwise
setExpanded(isExpanded ? id: null);
};
return (
<div className={styles.root}>
{accordions.map(accordion => {
const info = createAccordionInfo(accordion);
return (
<MyAccordion
key={info.uid} // <-- set React key here!!
onChange={handleChange(info.uid)}
expanded={expanded === info.uid}
/>
)
})}
</div>
);
};
Child
const MyAccordion =({ expanded, onChange }) => {
const classes = styles //?
return (
<Accordion
onChange={onChange}
expanded={expanded}
TransitionProps={{unmountOnExit: true}}
className={classes.accordion}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`${accordionInfo.uid}-content`}
id={`${accordionInfo.uid}-header`}
>
<Typography>Accordion Summary</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Accordion Details</Typography>
</AccordionDetails>
</Accordion>
);
};

Invalid Hook Call for React?

I'm trying to update the state of my component, but for some reason it keeps saying Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm trying to have a modal that opens onclick of a <div>. Within that modal is a text input that will update the state (notes). For some reason it's saying invalid hook call - why is that?
const openTestModal = () => {
let [notes, setNotes] = useState("");
let [openModal, setOpenModal] = useState(true);
let modalBody =
<div>
<TextInput
value={notes}
onChange={(value) => setNotes(value)}
/>
</div>
return (
<Modal
open={openModal}
onCancel={() => setOpenModal(false)}
onConfirm={() => console.log('works')}
body={modalBody}
/>
)
};
const TestHooks = () => {
return (
<div onClick={() => openTestModal()}>
Test
</div>
)
};
Seems like you tried to render testModal in react as an event, which's not a way to go, at all. Instead you must render your testModal as component, like that, so click on Test div will open your modal:
const TestModal = () => {
const [notes, setNotes] = useState("");
const modalBody = (
<div>
<TextInput
value={notes}
onChange={(value) => setNotes(value)}
/>
</div>
)
return (
<Modal
open={openModal}
onCancel={() => setOpenModal(false)}
onConfirm={() => console.log('works')}
body={modalBody}
/>
)
};
const TestHooks = () => {
const [openModal, setOpenModal] = useState(false);
return (
<React.Fragment>
<TestModal openModal={openModal} setOpenModal={setOpenModal} />
<div onClick={() => setOpenModal(true)}>
Test
</div>
<React.Fragment>
)
};
Hope it helps :)
It's not working because your testHooks is calling to setState from a separate component. Add you testHooks code into your openTestModal component. It would work as is if TestHooks was a child of openTestModal as well.

Writing a React hook to handle multiple divs visibility on mouse click

Just started to learn about Reack hooks but I cannot figure out if it is possible to write a simple hook (or should I use some other approach, e.g. useEffect along with useState) in order to control visibility of multiple elements by clicking on different buttons on page.
Let's say I have a simple app with 2 buttons and 2 "modal" windows:
const App = () => {
const [firstModalOpen, toggleFirstModal] = useState(false);
const [secondModalOpen, toggleSecondModal] = useState(false);
return (
<div>
<button onClick={() => toggleFirstModal(true)}>Open First Modal</button>
<button onClick={() => toggleSecondModal(true)}>Open Second Modal</button>
<FirstModal
{...props}
show={firstModalOpen}
toggleModal={toggleFirstModal}
/>
<SecondModal
{...props}
show={secondModalOpen}
toggleModal={toggleSecondModal}
/>
</div>
)
}
const FirstModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={firstModalOpen}
onHide={() => props.toggleModal(false)}
>
First modal content...
</Modal>
)
}
const SecondModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={secondModalOpen}
onHide={() => props.toggleModal(false)}
>
Second modal content...
</Modal>
)
}
// state hook attempt
const useToggleModal = () => (init) => {
const [show, setToggleModal] = useState(init);
const toggleModal = () => setToggleModal(!show);
return { show, toggleModal };
};
Since those are react-bootstrap modal windows, they use show and onHide properties to determine/handle visibility and I have to pass rest prop to avoid some side-effects.
If I'd use my hook attempt in my app, I'd handle both modals on any button click so I came up with the idea to pass a string (to both, buttons and modals) which would tell which modal exactly to handle, but that approach for some reason looked a bit wrong.
Is there a "smarter" way in React to handle this internally instead of passing strings around?
If you have multiple modals and only one of them needs to open at once, then you must use a single state which stores which modal is opened, kind of like a string having the id of the modal. However if you want to open multiple modals, you would store the isOpen prop differently
For the first case you would write your code like
const App = () => {
const [openModal, toggleModal] = useState('');
return (
<div>
<button onClick={() => toggleModal('first')}>Open First Modal</button>
<button onClick={() => toggleModal('second')}>Open Second Modal</button>
<FirstModal
{...props}
show={openModal === 'first'}
toggleModal={toggleModal}
/>
<SecondModal
{...props}
show={secondModalOpen}
toggleModal={toggleModal}
/>
</div>
)
}
const FirstModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={firstModalOpen}
onHide={() => props.toggleModal('first')}
>
First modal content...
</Modal>
)
}
const SecondModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={secondModalOpen}
onHide={() => props.toggleModal('second')}
>
Second modal content...
</Modal>
)
}
For the second case it would be as you have written in your example, the only optimisation you can do for the second case is to store an array of modal objects and render them dynamically or let each modal handle its own toggle states and use useImperativeHandle to provide methods which parent can call to child modals like
const App = () => {
const firstRef = useRef(null);
const secondRef = useRef(null);
return (
<div>
<button onClick={() => this.firstRef.current.toggleModal()}>Open First Modal</button>
<button onClick={() => this.secondRef.current.toggleModal()}>Open Second Modal</button>
<FirstModal
{...props}
ref={firstRef}
/>
<SecondModal
{...props}
ref={secondRef}
/>
</div>
)
}
const FirstModal = forwardRef((props, ref) => {
const { showModal, toggleModal } = useToggleModal(false, ref);
return (
<Modal
{ ...rest }
show={showModal}
onHide={toggleModal}
>
First modal content...
</Modal>
)
})
const SecondModal = forwardRef((props, ref) => {
const { showModal, toggleModal } = useToggleModal(false, ref);
return (
<Modal
{ ...props }
show={showModal}
onHide={toggleModal}
>
Second modal content...
</Modal>
)
})
// state hook attempt
const useToggleModal = (init, ref) => {
const [show, setToggleModal] = useState(init);
const toggleModal = () => setToggleModal(!show);
useImperativeHandle(ref, () => ({
toggleModal
}))
return { show, toggleModal };
};

Categories