react-testing-library testing Ant Design modal - reactjs

I have a React component with Ant Design Modal inside it and I am trying to test that modal gets opened when a button it clicked:
The component:
const ModalComponent = () => {
const [visible, setVisible] = useState(false);
return (
<>
<Button type="primary" onClick={() => setVisible(true)}>
Open Modal
</Button>
<Modal
title="Modal title"
centered
visible={visible}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
>
<p>some contents...</p>
<p>some contents...</p>
<p>some contents...</p>
</Modal>
</>
);
};
Test file:
test('modal opening', async () => {
const { queryByText } = render(<ModalComponent />);
fireEvent.click(queryByText('Open Modal'));
await waitFor(() => expect(queryByText('Modal title')).toBeInTheDocument());
});
The problem is that the modal DOM is never rendered in the test when I try to debug, so the test fails. It could be happening because the modal content is created outside of the component DOM tree right inside the body tag?

There is no test failure that you have given from our side.
A little information from my side on Antd modal component.
Antd Modal during testing renders outside the container. This is because Antd uses the rc-dialog component and that component uses react portal to show modal which is always render outside the root div. In the same way, during testing modal will not render in the container but outside of it.
The test that you have given will pass(modal is present) because the queryByText will search the element in document.body not inside the container.
test('modal opening', async () => {
const { baseElement, queryByText } = render(<ModalComponent />);
fireEvent.click(queryByText('Open Modal'));
expect(baseElement).toMatchSnapshot(); // added snapshot
await waitFor(() => expect(queryByText('Modal title')).toBeInTheDocument());
});
baseElement will show all the elements that are present in the document.body.

I had this exact same use case, with a very similar component.
For some reason, if I triggered the event inside act, then the state of the component wouldn't be updated and the modal would never come up (even if a console.log in the event handler does show up in the console).
The solution was to do userEvent.click outside the act

Related

Nested Modal Popups React or any other

Need modal inside modal if you click on edit then is one modal is opening. I need one more modal inside that opened modal
To create nested modal popups in React you can use a combination of modal components and conditional rendering.
Example
import React from 'react';
const ParentModal = () => {
const [showChildModal, setShowChildModal] = React.useState(false);
return (
<>
<button onClick={() => setShowChildModal(true)}>Show Child Modal</button>
{showChildModal && <ChildModal onClose={() => setShowChildModal(false)} />}
</>
);
};
const ChildModal = ({ onClose }) => {
const [showGrandchildModal, setShowGrandchildModal] = React.useState(false);
return (
<>
<button onClick={() => setShowGrandchildModal(true)}>Show Grandchild Modal</button>
{showGrandchildModal && <GrandchildModal onClose={() => setShowGrandchildModal(false)} />}
<button onClick={onClose}>Close</button>
</>
);
};
const GrandchildModal = ({ onClose }) => (
<>
<p>I'm the grandchild modal!</p>
<button onClick={onClose}>Close</button>
</>
);
export default ParentModal;
In the above example, we have three modal components: ParentModal, ChildModal, and GrandchildModal. The ParentModal component is the root of the nested modal popups, and it contains a button that will show the ChildModal when clicked. The ChildModal component, in turn, contains a button that will show the GrandchildModal when clicked. Each modal component also has a "Close" button that will close the modal when clicked.
We use the useState hook in each component to track the state of the modal (whether it is open or closed), and we use conditional rendering

Close antd popover and open a child antd modal in the same function

I have an Antd popover, that by clicking a button inside its content, opens a modal.
I want to close the popover when the modal opens.
When I tried just passing the popover visibility state setter down to the modal as a prop, there was a problem. There was some kind of "collision" between the state of the modal and the passed down prop state of the popover:
Collision CodeSandbox example
I did find a workaround - creating the modal state variables in the parent component (the popover) and passing them down to the modal using props:
Working CodeSandbox example
First of all, you can notice that the modal isn't closing at it supposed to - there's no nice smooth animation minimizing it, it just suddenly disappears. For reference, you can look here to see how it should look like when closing.
So my question is - why did this collision happen? Is there a better way to solve it?
Thanks!
This collision happens because in show modal handler you set visibility of popover to false and hide it and ant-popover-hidden class add to it's div element so anything inside it would not display like Modal however you show modal but because of its parent it couldn't visible, so I think You must separate modal from the popover content and place it somewhere beside them like this:
const Test = () => {
const [isSharePopoverVisible, setIsSharePopoverVisible] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const handlePopoverVisibleChange = () => {
setIsSharePopoverVisible(!isSharePopoverVisible);
};
const handleOk = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const showModal = () => {
setIsModalVisible(true);
setIsSharePopoverVisible(false);
};
return (
<>
<Popover
trigger="click"
title="Test"
visible={isSharePopoverVisible}
onVisibleChange={handlePopoverVisibleChange}
content={
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
}
>
<Button>Test</Button>
</Popover>
<Modal
title="Basic Modal"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<p>Some contents...</p>
</Modal>
</>
);
};

Material-UI v5 Dialog exit fade animation not working with React Router

I'm editing the React Router Modal Gallery example from their docs to include Material-UI dialogs. The problem is the exit animation (fade out) does not run because the URL changes as you close the modal, so it just disappears.
Is there a way to retain the current functionality and add the pleasant fade out?
What I need is very similar to this, but when you refresh the page on that example, the modal is still open, I need it to open without the modal on page refresh, just like in the codesandbox and default React Router example.
Note: this issue is not specific to Mui5, I just happen to be using it.
Not sure if it is what you want and not sure if it is the best approach.
You can manipulate the transition manually and do whatever you want when the animation is done via onExited() method.
const Transition = React.forwardRef((props, ref) => {
const history = useHistory();
return (
// use Slide to display a clearer transition,
// can replace it with Fade
<Slide
ref={ref}
{...props}
// do something after animation exited
onExited={() => {
history.goBack();
}}
/>
);
});
function Modal(props) {
// open state is used for transition trigger
const [open, setOpen] = React.useState(props.open);
const { id } = useParams();
const image = IMAGES[parseInt(id, 10)];
if (!image) return null;
function handleClose() {
// setting false to trigger exit animation
setOpen(false);
}
return (
<Dialog
open={open}
onClose={handleClose}
TransitionComponent={Transition}
scroll="body"
>
<Content />
</Dialog>
);
}
Here is the codesandbox for demo.

Reusable Modal Component React Typescript

I have a component which has a button within it, like so -
<Button variant="primary" disabled={checkAccepted} onClick={openModal}>Send</Button>
I would like this button to, when it is active, to open up a modal when clicked. I am unsure how to do this and have been messing around with props but can't seem to figure it out. I also want the modal to be reusable so that any content can be passed in the modal body.I am thinking how do I open up the modal from within my openModal function?
I tried returning it like so -
const openModal = () => {
return (
<Modal>
<ModalBody>*Pass in swappable content here*</ModalBody>
</Modal>
)
}
But that doesn't seem to work. I am sure I am missing something.
You can't return components from an event handler. The way to handle events in react is almost always to alter the state of your application which triggers a re-render. In your case you need to keep track of the open state of your modal.
This can be done either in a controlled way (you keep track of the open state yourself and pass it to your <Modal> component as a prop) or in an uncontrolled way (the <Modal> component manages the open state itself). The second approach requires that you provide e.g. an element to render to your Modal component that acts as a trigger:
const MyModal = ({ children, trigger }) => {
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
return (
<div>
{React.cloneElement(trigger, { onClick: toggle })}
<Modal isOpen={modal} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
};
Then you can use it like that:
<MyModal trigger={<Button variant="primary">Send</Button>}>
<p>This is the content.</p>
</MyModal>
Or you can implement it in a controlled way. This is more flexible as it allows you to render the triggering element anywhere:
const MyModal = ({ children, isOpen, toggle }) => (
<div>
<Modal isOpen={isOpen} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
Usage Example:
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<div className="App">
<Button variant="primary" onClick={toggle}>
Send
</Button>
<MyModal isOpen={isOpen} toggle={toggle}>
<p>This is the content.</p>
</MyModal>
</div>
);
}
You should pass the function which triggers the modal to your <Button /> component as prop. Then, in your component, you want to add the onClick event. You can't set an onClick event to the <Button />. It will think of onClick as a prop being passed to <Button />. Within <Button /> you can set the onClick event to an actual <button> element, and use the function which was passed in as a prop on that event.
You can use state to keep track of when the modal button is clicked. Your function can look like: (I am using class based components here, but you can do the same thing with functional components)
buttonClickedHandler = () => {
this.setState({isModalButtonClicked: !this.state.isModalButtonClicked});
}
Then, you can set the Modal component,
<Modal isShow={this.state.isModalButtonClicked} modalButton={this.buttonClickedHandler}>
<div> ...set contents of modal</div>
</Modal>
<button onClick={this.buttonClickedHandler}>Show Modal</button>
So, within the Modal component, you can have something like this:
<React.Fragment>
<Backdrop showModal={this.props.isShow} clicked={this.props.modalButton}/>
{this.props.children}
</React.Fragment>
Backdrop is basically the greyed out background. You can also set an onClick event to listen to when the backdrop is clicked.

React for displaying the log file in real-time (Flask backend)

I have a working code (Flask + plain JS) which shows the growing log file in real time.
I have a problem converting it to ReactJS.
app.py (Flask, working code, no problems)
#app.route('/stream')
def stream():
def generate():
fname="/Users/xxx/tmp/log.txt"
with open(fname) as f:
while True:
yield f.read()
sleep(1)
return
app.response_class(generate(), mimetype='text/plain')
app.run()
index.html (plain JS, no problems)
<pre id="output"></pre>
<script>
var output = document.getElementById('output');
var xhr = new XMLHttpRequest();
xhr.open('GET', '');
xhr.send();
setInterval(function() {
output.textContent = xhr.responseText;
}, 1000);
</script>
Below is my attempt to convert it to React.
On MyPage.jsx I put the button "Show Real Time Log".
When user clicks this button the Modal Dialog should pop-up and display the log.
File: MyPage.jsx
import Modal from 'react-bootstrap/Modal'
import { Button } from 'react-bootstrap'
const showLog = (e) => {
render(<ModalLog />, document.getElementById('output'));
}
const MyPage = () => {
return (
<div>
<div id="output"></div>
<button type="button" onClick={showLog}>Show Real Time Log</button>
</div
}
function ModalLog() {
const [show, setShow] = useState(true);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Real Time Log</Modal.Title>
</Modal.Header>
<Modal.Body> Log file live update goes here </Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
Questions:
Where to put into MyPage.jsx the JS code currently sitting in my index.html?
When I press the button "Show Real Time Log" the Modal is displayed only 1st time, after I close it never displayed again. I understand what it is because handleClose() was called, but how to fix it?
(1) Where to put into MyPage.jsx the JS code currently sitting in my index.html?
Simple answer is you don't. It's directly manipulating the DOM and this is a severe react anti-pattern. You would convert it to an useEffect hook to fetch the latest log data and save it to component state, to be rendered.
const [logs, setLogs] = useState('');
useEffect(() => {
// asynchronous function to fetch logs
const fetchLogs = async () => {
try {
const logResponse = await /* logic to fetch log */
setLogs(logResponse);
} catch {
// just don't update state, or set an error message in state,
// basically anything you want to do to handle errors
}
}
// setup interval to fetch logs
const intervalTimer = setInterval(fetchLogs, 1000);
// function to clean up effect when component unmounts
return () => clearInterval(intervalTimer);
}, []); // empty dependency array to run when mounted
(2) When I press the button "Show Real Time Log" the Modal is displayed only 1st time, after I close it never displayed again. I understand what it is because handleClose() was called, but how to fix it?
You initial state to display the modal is true. I would refactor the logic a bit to have MyPage hold the state if the modal is open or not (instead of the modal). Pass show and handleClose as props to ModalLog. No need to render the modal into another DOM element, I believe the react-bootstrap will handle creating a portal for you. You can simply render it into your component.
const MyPage = () => {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<div>
<div id="output"></div>
<button type="button" onClick={handleShow}>Show Real Time Log</button>
<ModalLog show={show} onHide={handleClose} />
</div>
);
}
function ModalLog({ show, onHide }) {
return (
<>
<Modal show={show} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>Real Time Log</Modal.Title>
</Modal.Header>
<Modal.Body> Log file live update goes here </Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
Suggestion: Place the log state and logic to fetch and display the logs in the modal as it appears you only care to display them when the modal is open and there's no point in fetching them in the background while the modal is closed.

Resources