How to test Material-UI Popover close implementation - reactjs

I would like to make sure that my implementation of a Popover element combined with a trigger button works as expected.
I was unable to have a working test for asserting that the Popover gets closed after the user presses esc. I was able to make this test work with Modal, but I must use a Popover in my current project.
Component code:
import {
Button, Popover,
} from '#mui/material';
import React from 'react';
export default function SimpleModal() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'user-menu' : undefined;
return (
<>
<Button
aria-controls={id}
aria-haspopup="true"
onClick={handleClick}
>
Open Modal
</Button>
<Popover open={open} onClose={handleClose}>
<h1>Text in Modal</h1>
</Popover>
</>
);
}
Test file code:
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SimpleModal from './SimpleModal';
test('should close when esc key is pressed', async () => {
render(<SimpleModal />);
userEvent.click(screen.getByText('Open Modal'));
expect(screen.getByText('Text in Modal')).toBeInTheDocument();
userEvent.keyboard('{esc}');
await expect(screen.queryByText('Text in Modal')).not.toBeInTheDocument();
});

As suggested by #juliomalves, wrapping the last expect in a waitFor corrected the test:
await waitFor(() => expect(...));

Related

How to use anchorEl with styled() in MUI?

I'm trying to anchor a popover component to a button component. The problem is that this doesn't seem to work if the button is styled using styled() (I'm using emotion).
This code causes the following warning: MUI: The `anchorEl` prop provided to the component is invalid.
Because anchorEl is invalid the popover will simply postion itself on the top left corner of the screen.
import { useState } from "react";
import { styled } from "#mui/material/styles";
import Popover from "#mui/material/Popover";
import Button from "#mui/material/Button";
export default function BasicPopover() {
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const StyledButton = styled((props) => <Button {...props} />)(
({ theme }) => ({
//some styling
})
);
return (
<div>
<StyledButton
variant="contained"
onClick={handleClick}
>
Open Popover
</StyledButton>
<Popover open={open} anchorEl={anchorEl} onClose={handleClose}>
The content of the Popover.
</Popover>
</div>
);
}
I found a slightly different approach using refs here, but I couldn't figure out how to make it work with styled() either.
I'm still rather new to react so please be gentle.
I don't know why, but if you move the styled() out of the main component it works.
const StyledButton = styled((props) => <Button {...props} />)(
({ theme }) => ({
//some styling
})
);
export default function BasicPopover() {
//[...]
}

Ionic : Hardware Back Button Event Listener not executed when modal is open

In ionic framework when the hardware back button is pressed the following event listener method is executed.
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(10, () => {
console.log('Handler was called!');
});
});
But when a modal kept opened then the above method is not executed after pressing the hardware back button. It shows only the following message on the console of android studio
Notifying listeners for event backButton
Updated :
the following code is for the modal in ionic react
import React, { useState } from 'react';
import { IonModal, IonButton, IonContent } from '#ionic/react';
export const ModalExample: React.FC = () => {
const [showModal, setShowModal] = useState(false);
return (
<IonContent>
<IonModal isOpen={showModal} cssClass='my-custom-class'>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
<IonButton onClick={() => setShowModal(true)}>Show Modal</IonButton>
</IonContent>
);
};
I have found the solution for triggering the hardware back button event listener method, simply by increasing the priority up to 140.
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(140, () => {
console.log('Handler was called!');
});
});

How to prevent re-render when using react material ui dialogs

I'm using modals from the react-material-ui library for a project, and I noticed a side effect when trying to open/close the dialog component. Try this code (code sandbox):
import { useState } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
export default function App() {
const [open, setOpen] = useState(false);
const timeStamp = moment().format("HH:mm:ss SS");
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<Dialog open={open}>
<DialogTitle>This is a simple dialog</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
You'll see that clicking on the button open will cause a re-render and closing the dialog will have the same effect, it's normal because state changed after calling hooks. This is often undesirable, I don't want to re-render the page when opening or after closing.
So I tried to use the following solution, based of refs (code sandbox):
import { useState, useRef, forwardRef, useImperativeHandle } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
const SimpleDialog = forwardRef(({ title }, ref) => {
const [open, setOpen] = useState(false);
const innerRef = useRef();
const handleClose = () => {
setOpen(false);
};
useImperativeHandle(ref, () => ({
openDialog: () => setOpen(true),
closeDialog: () => setOpen(false)
}));
return (
<Dialog open={open} ref={innerRef}>
<DialogTitle>{title}</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
);
});
export default function App() {
const timeStamp = moment().format("HH:mm:ss SS");
const dialogRef = useRef();
const handleOpen = () => {
dialogRef.current.openDialog();
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<SimpleDialog ref={dialogRef} title="This is a simple dialog" />
</div>
);
}
My question is: is this a correct approach to solve the problem of unwanted re-renders in the case of react-material-ui modals ?
Regards.

Show MaterialUi snackbar by method

I want to show message in material.ui by only call method not ading component to parent component (like toastify.js). So, I wrote example like below. But I couldn't call showSnack() method. How can I achieve this?
Note: I don't want add component to demo js like < SnackbarHelper />. I only want show snackbar calling by function.
CODESANDBOX LINK
Demo.js
import React from "react";
import Button from "#material-ui/core/Button";
import SnackHelper from "./snackHelper";
export default function PositionedSnackbar() {
function showMessage() {
console.log("I want call snackHelper.showSnack");
// snackHelper.showSnack();
}
return (
<div>
<Button variant="contained" onClick={() => showMessage()}>
SHOW MESSAGE
</Button>
</div>
);
}
snackbarHelper.js
import React from "react";
import Snackbar from "#material-ui/core/Snackbar";
export default function SnackHelper() {
const [state, setState] = React.useState({
open: false
});
const { vertical, horizontal, open } = state;
const showSnack = (newState) => () => {
setState({ open: true, ...newState });
};
const handleClose = () => {
setState({ ...state, open: false });
};
return (
<div>
<Snackbar
anchorOrigin={{ vertical, horizontal }}
open={open}
onClose={handleClose}
message=""
key={vertical + horizontal}
/>
</div>
);
}
I found solution in this article for same thing what I was looking. Only difference is, this is for confirmation dialog and written by typescript. But, it can be easily changed to toast message by javascript. https://dev.to/dmtrkovalenko/the-neatest-way-to-handle-alert-dialogs-in-react-1aoe
You can get working example code https://codesandbox.io/s/neat-dialogs-3h5ou?from-embed=&file=/src/ConfirmationService.tsx

How can I test react toggle with jest+Enzyme?

Hi Recently I'm testing my react application with jest+enzyme,
and it makes me really confused to write a test with useState or useEffect..
so this is my code and I want to test when user click the button, description is shows or not.(by change state value)
const Job = ({ job }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = (id) => {
setIsOpen(!isOpen);
};
return (
<Card>
<Card.Text>
<Button id="button" onClick={toggle} variant="primary">
{!isOpen ? "View Detail" : "Hide Detail"}
</Button>
</Card.Text>
{isOpen && (
<div className="mt-4">
<ReactMardown source={job.description} />
</div>
)}
</Card>
);
};
export default Job;
Job.test.js
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import { mount, shallow, configure } from "enzyme";
import Job from "./Job";
configure({ adapter: new Adapter() });
describe("when user click the button state value should changed", () => {
const job = jest.fn();
let wrapper;
beforeEach(() => {
wrapper = mount(<Job job={job} />);
});
it("should render", () => {
expect(wrapper).not.toBeNull();
});
test("user click button", () => {
const setIsOpen = jest.fn();
wrapper.find("#button").at(1).simulate("click");
expect(setIsOpen).toHaveBeenLastCalledWith({ isOpen: false });
});
});
but it only passed the first test..
#nambk I'd go with testing the style, eg. some component's background-color:
expect(component.find('#item-id').prop('style')).toHaveProperty('backgroundColor' 'black');
It's generally better, especially in stateless components, not to test the implementation, but its result.
it('user click button', () => {
expect(wrapper.find(ReactMardown)).toHaveLength(0);
wrapper.find('#button').simulate('click');
expect(wrapper.find(ReactMardown)).toHaveLength(1);
});

Resources