React Testing Library: Testing boolean state - javascript

I am currently writing a test using the testing-library/react package.
On the second click of my test, the text should not be in the document, but it is.
Here is the component I am testing:
import { useState } from 'react'
import { InfoType } from '../../constants'
type Props = {
type: InfoType
}
const HelpWidget = ({ type }: Props) => {
const [isHover, setHover] = useState<boolean>(false)
return (
<div className="help-widget">
<img
src={require('../../../public/images/info.svg')}
onMouseEnter={() => setHover(true)}
onMouseOut={() => setHover(false)}
onClick={() => setHover(!isHover)}
/>
{isHover ? <div className="info">{type}</div> : <div></div>}
</div>
)
}
export default HelpWidget
Here is the test I wrote:
import React from 'react'
import { screen, render, waitFor } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import HelpWidget from '../components/helpWidget/helpWidget'
// Testing if the HelpWidget conditionally renders based on the isHover boolean state.
test('helpWidget conditionally renders to the page', () => {
const epk4Post = 'Select a persona to post this'
render(<HelpWidget type={epk4Post} />)
const imageElement = screen.getByRole("img")
userEvent.click(imageElement)
screen.debug()
expect(screen.getByText(/select a persona to post this/i)).toBeInTheDocument()
userEvent.click(imageElement)
screen.debug()
});
And here is what I get when I run screen.debug() to see the DOM output. Here you can clearly see that on the second click, the text is still within the document when it should not be:
Please share your thoughts! Thank you.

Related

React: How to setState in child component from parent component?

I'm new to React and am attempting to set up a Bootstrap modal to show alert messages.
In my parent App.js file I have an error handler that sends a Modal.js component a prop that triggers the modal to show, eg:
On App.js:
function App() {
const [modalShow, setModalShow] = useState(false);
// Some other handlers
const alertModalHandler = (modalMessage) => {
console.log(modalMessage);
setModalShow(true);
}
return (
// Other components.
<AlertModal modalOpen={modalShow}/>
)
}
And on Modal.js:
import React, { useState } from "react";
import Modal from "react-bootstrap/Modal";
import "bootstrap/dist/css/bootstrap.min.css";
const AlertModal = (props) => {
const [isOpen, setIsOpen] = useState(false);
if (props.modalOpen) {
setIsOpen(true);
}
return (
<Modal show={isOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
export default AlertModal;
However, this doesn't work. I get the error:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
If I change the Modal component to be a 'dumb' component and use the prop directly, eg:
const AlertModal = (props) => {
return (
<Modal show={props.modalOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
It does work, but I was wanting to change the show/hide state on the Modal.js component level as well, eg have something that handles modal close buttons in there.
I don't understand why is this breaking?
And does this mean I will have to handle the Modal close function at the parent App.js level?
Edit - full app.js contents
import React, { useState } from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.css';
import AddUserForm from './components/addUserForm';
import UserList from './components/userList';
import AlertModal from './components/modal';
function App() {
const [users, setUsers] = useState([]);
const [modalShow, setModalShow] = useState(false);
const addPersonHandler = (nameValue, ageValue) => {
console.log(nameValue, ageValue);
setUsers(prevUsers => {
const updatedUsers = [...prevUsers];
updatedUsers.unshift({ name: nameValue, age: ageValue });
return updatedUsers;
});
};
const alertModalHandler = (modalMessage) => {
console.log(modalMessage);
setModalShow(true);
}
let content = (
<p style={{ textAlign: 'center' }}>No users found. Maybe add one?</p>
);
if (users.length > 0) {
content = (
<UserList items={users} />
);
}
return (
<>
<div className="container">
<div className="row">
<div className="col-md-6 offset-md-3">
<AddUserForm onAddPerson={addPersonHandler} fireAlertModal={alertModalHandler}/>
</div>
</div>
<div className="row">
<div className="col-md-6 offset-md-3">
{content}
</div>
</div>
</div>
<AlertModal modalOpen={modalShow}/>
</>
);
}
export default App;
In your modal.js
you should put
if (props.modalOpen) {
setIsOpen(true);
}
in a useEffect.
React.useEffect(() => {if (props.modalOpen) {
setIsOpen(true);
}}, [props.modalOpen])
You should never call setState just like that. If you do it will run on every render and trigger another render, because you changed the state. You should put the setModalShow together with the if clause in a useEffect. E.g.:
useState(() => {
if (modalOpen) {
setIsOpen(true);
}
}, [modalOpen])
Note that I also restructered modalOpen out of props. That way the useEffect will only run when modalOpen changes.
If you already send a state called modalShow to the AlertModal component there is no reason to use another state which does the same such as isOpen.
Whenever modalShow is changed, it causes a re-render of the AlertModal component since you changed it's state, then inside if the prop is true you set another state, causing another not needed re-render when you set isOpen. Then, on each re-render if props.showModal has not changed (and still is true) you trigger setIsOpen again and again.
If you want control over the modal open/close inside AlertModal I would do as follows:
<AlertModal modalOpen={modalShow} setModalOpen={setModalShow}/>
Pass the set function of the showModal state to the modal component, and there use it as you see fit. For example, in an onClick handler.
modal.js:
import React, { useState } from "react";
import Modal from "react-bootstrap/Modal";
import "bootstrap/dist/css/bootstrap.min.css";
const AlertModal = (props) => {
const onClickHandler = () => {
props.setModalOpen(prevState => !prevState)
}
return (
<Modal show={props.modalOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
export default AlertModal;

hook call error: Hooks can only be called inside of the body of a function component

I'm new to react component, getting this error when using react-hooks, here is my code, can anyone help me with that? This is the detail of the error.
This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
const newParams = "params"
// eslint-disable-next-line react-hooks/rules-of-hooks
const history = useHistory();
// eslint-disable-next-line react-hooks/rules-of-hooks
const location = useLocation();
export class FullscreenDialog extends React.Component{
constructor(props: any) {
super(props);
this.state = { dialogShow: false };
this.onDismissFullscreen = this.onDismissFullscreen.bind(this);
this.openDialogWithButton = this.openDialogWithButton.bind(this);
}
openDialogWithButton() {
updateSearch({history, location, newParams})
}
onDismissFullscreen() {
closeDialog({ history, location, key: 'key' })
}
render(){
const uniqueDialogId3 = 'notes';
return (
<>
<DialogRoute id={uniqueDialogId3}>
<Layer id="fullscreenDialog" >
<UitkFullscreenDialog ariaLabel="Demo" dialogShow={true} returnFocusOnClose={true}>
<UitkToolbar
header="Toolbar heading"
iconLabel="Close the dialog"
key="UitkToolbar"
type={ToolbarType.CLOSE}
/>
<UitkDialogContent key="UitkDialogContent-1">
<UitkParagraph key="UitkDialogContentParagraph" size={2}>
test
</UitkParagraph>
</UitkDialogContent>
</UitkFullscreenDialog>
</Layer>
</DialogRoute>
<UitkLink inline={true}>
<button onClick={this.openDialogWithButton}>Open Fullscreen Dialog</button>
</UitkLink>
</>
);
}
}
A general rule of thumb is anything starting with "use" is going to be a hook and will not work in a class based component. You can Wrap your component in withRouter from react router dom to access the history prop that way if you want to stick with a class based component, check that out here. Otherwise you can switch to a functional component which would look like this:
import React, {useState} from 'react';
const newParams = "params"
// eslint-disable-next-line react-hooks/rules-of-hooks
const history = useHistory();
// eslint-disable-next-line react-hooks/rules-of-hooks
const location = useLocation();
const FullscreenDialog = ({closeDialog, updateSearch}) => {
const [dialogShow, setDialogShow] = useState(false)
const openDialogWithButton = () => {
updateSearch({history, location, newParams})
}
const onDismissFullscreen = () => {
closeDialog({ history, location, key: 'key' })
}
const uniqueDialogId3 = 'notes';
return (
<>
<DialogRoute id={uniqueDialogId3}>
<Layer id="fullscreenDialog" >
<UitkFullscreenDialog ariaLabel="Demo" dialogShow={true} returnFocusOnClose={true}>
<UitkToolbar
header="Toolbar heading"
iconLabel="Close the dialog"
key="UitkToolbar"
type={ToolbarType.CLOSE}
/>
<UitkDialogContent key="UitkDialogContent-1">
<UitkParagraph key="UitkDialogContentParagraph" size={2}>
test
</UitkParagraph>
</UitkDialogContent>
</UitkFullscreenDialog>
</Layer>
</DialogRoute>
<UitkLink inline={true}>
<button onClick={openDialogWithButton}>Open Fullscreen Dialog</button>
</UitkLink>
</>
)
}

Button function is not working in React Hooks using external components

I have the next code, where I import NextButton and GroupButton from TitleHeader,
those components are simple buttons
After that, I declared a simple array ButtonsArray and filled it with those components in the useEffect segment, in adition, I 'bind' the Button function to the button component.
Example :
<NextButton function={ShowSearchBar}/>
Then, my other component TitleHeader receives the array and render the components inside it using a map function
My issue is, if I use the const array ButtonsArray with the components loaded as props in TitleHeader, when press the NextButton in the UI to confirm everything is working something weird happens
The only job of NextButton is execute ShowSearchBar function whose have to switch a const from true to false and vice versa but it doest not work,
If i debug the program, when I press the button, the program enters to the ShowSearchBar function but ALWAYS allowFind is false
Note: if I declare the array directly in the TitleHeader params everything works fine
import React, { useState, useEffect } from "react";
import { TitleHeader, NextButton, GroupButton } from "../Common/TitleHeader";
export const ACATG001 = () => {
const [allowFind, setAllowFind] = useState(false);
const [allowGroup, setAllowGroup] = useState(false);
const [ButtonsArray, setButtonsArray] = useState([]);
useEffect(() => {
setButtonsArray([
<NextButton function={ShowSearchBar} />,
<GroupButton function={ShowGroupBar} />,
]);
}, []);
function ShowSearchBar() {
setAllowFind(!allowFind);
}
return (
<GeneralContainer>
//doesnt work (using a const type array and filled in UseEffect)
<TitleHeader
Title={t("TTER001")}
BarSize="300px"
Embedded={false}
ButtonsArray={ButtonsArray}
/>
//Works declaring the array and the items inline
<TitleHeader
Title={t("TTER001")}
BarSize="300px"
Embedded={false}
ButtonsArray={[
<NextButton function={ShowSearchBar} />,
<GroupButton function={ShowGroupBar} />,
]}
/>
</GeneralContainer>
);
};
Second JS TitleHeader
import React, { Component } from "react";
import { Button } from "primereact/button";
export class TitleHeader extends Component {
constructor() {
super();
}
componentDidMount() {}
render() {
let TitleDesing;
TitleDesing = (
<div className="Buttons-Group">
{this.props.ButtonsArray.map((component, index) => (
<React.Fragment key={index}>{component}</React.Fragment>
))}
</div>
);
return TitleDesing;
}
}
export const NextButton = (props) => {
return (
<Button
id="nextButton"
label="test"
tooltip="Next"
className="p-button-rounded p-button-text"
onClick={props.function}
>
<CgChevronRight size="20PX" color=" #d6f1fa" />{" "}
</Button>
);
};
If the update you do to a state depends only on its current value, always use the function callback version of the dispatcher, this will guarantee you don't use a stale value
function ShowSearchBar() {
setAllowFind((previousAllowFind) => !previousAllowFind)
}

How to set data from a react hook inside of a function or event?

I'm trying to learn to create hooks so I can re-use data that I have to change in different components.
I'm using Material UI's Tabs and need to use useTab, a custom hook to change the tab id.
import React, { useContext } from 'react';
import { ProductsContext } from './ProductsContext';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { useTab } from '../../hooks/tab';
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue] = useTab(0);
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};
export default ProductsNav;
I know it does this with child functions in the docs, but I'm trying to not just copy and paste and do it in my own way.
Here is my custom useTab hook:
import {useState, useEffect} from 'react';
export const useTab = (selectedTab) => {
const [tabValue, setTabValue] = useState(0);
useEffect(() => {
setTabValue(selectedTab);
}, []);
return [tabValue];
}
I'm of course getting an error I can't use a hook inside of a function, but I'm confused how else to do this.
How can I change tabValue from useTabs?
The error is probably here:
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
You're violating one of the primary Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function.
The reason for this rule is a bit complex but it basically boils down to the idea that hooks should only be called at the top level of a React functional component because they must be guaranteed to run every time the component function is run.
Hence why you're getting an error "I can't use a hook inside of a function"...
At any rate, it is unclear why you are using a custom hook with a useEffect() here. That seems completely unnecessary - a regular useEffect() hook inside of your nav component should more than suffice:
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue, setTabValue] = useState(0);
const handleTabChange = (e, newTabValue) => {
setTabValue(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};

React-Loadable re-rendering causing input to lose focus

I'm having an issue where react-loadable is causing one of my input components to re-render and lose focus after a state update. I've done some digging and I can't find anyone else having this issue, so I think that I'm missing something here.
I am attempting to use react-loadable to dynamically include components into my app based on a theme that the user has selected. This is working fine.
./components/App
import React from 'react';
import Loadable from 'react-loadable';
/**
* Import Containers
*/
import AdminBar from '../../containers/AdminBar';
import AdminPanel from '../../components/AdminPanel';
import 'bootstrap/dist/css/bootstrap.css';
import './styles.css';
const App = ({ isAdmin, inEditMode, theme }) => {
const MainContent = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/MainContent'),
loading: () => (<div>Loading...</div>)
});
const Header = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/Header'),
loading: () => (<div>Loading...</div>)
});
return (
<div>
{
(isAdmin) ? <AdminBar
className='admin-bar'
inEditMode={inEditMode} /> : ''
}
<Header
themeSettings={theme.settings.Header} />
<div className='container-fluid'>
<div className='row'>
{
(isAdmin && inEditMode) ? <AdminPanel
className='admin-panel'
theme={theme} /> : ''
}
<MainContent
inEditMode={inEditMode} />
</div>
</div>
</div>
);
};
export default App;
./components/AdminPanel
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const AdminPanel = ({ theme }) => {
const ThemedSideBar = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/SideBar'),
loading: () => null
});
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
This is what my <ThemedSideBar /> components looks like:
./themes/Default/components/SideBar
import React from 'react';
import ThemeSettingPanel from '../../../../components/ThemeSettingPanel';
import ThemeSetting from '../../../../containers/ThemeSetting';
import './styles.css';
const SideBar = ({ settings }) => {
return (
<ThemeSettingPanel
name='Header'>
<ThemeSetting
name='Background Color'
setting={settings.Header}
type='text'
parent='Header' />
<ThemeSetting
name='Height'
setting={settings.Header}
type='text'
parent='Header' />
</ThemeSettingPanel>
);
};
export default SideBar;
./components/ThemeSettingPanel
import React from 'react';
import { PanelGroup, Panel } from 'react-bootstrap';
const ThemeSettingPanel = ({ name, children }) => {
return (
<PanelGroup accordion id='sidebar-accordion-panelGroup'>
<Panel>
<Panel.Heading>
<Panel.Title toggle>{name}</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
{children}
</Panel.Body>
</Panel>
</PanelGroup>
);
};
export default ThemeSettingPanel;
./containers/ThemeSetting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { themeSettingChange } from '../App/actions';
import ThemeSetting from '../../components/ThemeSetting';
class ThemeSettingContainer extends Component {
constructor(props) {
super(props);
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange(name, parent, value) {
const payload = {
name: name,
parent,
value: value
};
this.props.themeSettingChange(payload);
}
render() {
return (
<ThemeSetting
name={this.props.name}
setting={this.props.setting}
parent={this.props.parent}
type={this.props.type}
handleOnChange={this.handleOnChange} />
);
}
}
//----Redux Mappings----//
const mapStateToProps = (state) => ({
});
const mapDispatchToProps = {
themeSettingChange: (value) => themeSettingChange(value)
};
export default connect(mapStateToProps, mapDispatchToProps)(ThemeSettingContainer);
./component/ThemeSetting
import React from 'react';
import TextField from '../common/TextField';
import './styles.css';
const ThemeSetting = ({ name, setting, type, parent, handleOnChange }) => {
return (
<div className='row theme-setting'>
<div className='col-xs-7'>
{name}
</div>
<div className='col-xs-5'>
{
generateField(type, setting, name, parent, handleOnChange)
}
</div>
</div>
);
};
function generateField(type, setting, name, parent, handleOnChange) {
const value = setting ? setting[name] : '';
switch (type) {
case 'text':
return <TextField
value={value}
name={name}
parent={parent}
handleOnChange={handleOnChange} />;
default:
break;
}
}
export default ThemeSetting;
./components/common/TextField
import React from 'react';
import { FormControl } from 'react-bootstrap';
const TextField = ({ value, name, parent, handleOnChange }) => {
return (
<FormControl
type='text'
value={value}
onChange={(e) => {
handleOnChange(name, parent, e.target.value);
}} />
);
};
export default TextField;
When a field inside of my Admin Panel is updated, a state change is triggered. It seems like this triggers react-loadable to re-render my <ThemedSideBar /> components which destroys my input and creates a new one with the updated value. Has anyone else had this issue? Is there a way to stop react-loadable from re-rendering?
EDIT: Here is the requested link to the repo.
EDIT: As per conversation in the comments, my apologies, I misread the question. Answer here is updated (original answer below updated answer)
Updated answer
From looking at the react-loadable docs, it appears that the Loadable HOC is intended to be called outside of a render method. In your case, you are loading ThemedSideBar in the render method of AdminPanel. I suspect that the change in your TextEdit's input, passed to update your Redux state, and then passed back through the chain of components was causing React to consider re-rendering AdminPanel. Because your call to Loadable was inside the render method (i.e. AdminPanel is a presentational component), react-loadable was presenting a brand new loaded component every time React hit that code path. Thus, React thinks it needs to destroy the prior component to appropriately bring the components up to date with the new props.
This works:
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const ThemedSideBar = Loadable({
loader: () => import('../../themes/Default/components/SideBar'),
loading: () => null
});
const AdminPanel = ({ theme }) => {
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
Original answer
It seems that your problem is likely related to the way you've built TextField and not react-loadable.
The FormControl is taking value={value} and the onChange handler as props. This means you've indicated it is a controlled (as opposed to uncontrolled) component.
If you want the field to take on an updated value when the user types input, you need to propagate the change caught by your onChange handler and make sure it gets fed back to the value in the value={value} prop.
Right now, it looks like value will always be equal to theme.settings.Height or the like (which is presumably null/empty).
An alternative would be to make that FormControl an uncontrolled component, but I'm guessing you don't want to do that.

Categories