Tooltip MUI and React testing library - material-ui

I'm trying to test an Info HOC on my react app :
const InfoHOC = (HocComponent) => ({ message }) => (
<>
<Tooltip title={message}>
<InfoIcon />
</Tooltip>
{HocComponent}
</>
);
export default InfoHOC;
I've simplified it. But as it's using material ui Tooltip component, I can't test if message is displayed on mouseover...
it('should display info message on <div /> mouseover', () => {
const Component = InfoHoc(<div>jest div</div>)({ message: 'jest infoHoc message' });
const { getByTitle, getByDisplayValue } = render(Component);
const icon = getByTitle('jest infoHoc message');
act(() => {
fireEvent(
icon,
new MouseEvent('mouseover', {
bubbles: true,
}),
);
});
expect(getByDisplayValue('jest infoHoc message')).toBeInTheDocument();
});
My last line is wrong... I think it's because mui tooltip display the message in a div at the end of the body, so not really in my rtl tree... BUT the first element of this tree is body !
I know that I should not test mui component, but here is not the purpose, I just want to be sure that InfoHoc has the right comportment, using mui tooltip or something else.
Here is the RTL tree after mouseover action :
<body>
<div>
<div
class="infoHoc"
>
<div>
jest div
</div>
<svg
aria-hidden="true"
class="MuiSvgIcon-root icon--right"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
/>
</svg>
</div>
</div>
</body>
The event is good because icon has a title attr with message as value till mouseover is fired. As title attr is not here on my tree, I assume my event is well executed ;p
I am wrong testing that ? If not do you have an idea to solve my problem ?
Thank you all !

I think this is the cleanest way.
it('Renders tooltip when hovering over button', async () => {
render(<Search />);
const button = await screen.findByRole('button');
await userEvent.hover(button);
const tip = await screen.findByRole('tooltip');
expect(tip).toBeInTheDocument();
});

In case this can still help you, you need to findBy instead of getBy as the Tooltip is showing the tooltip after a delay
it('should display info message on <div /> mouseover', async () => {
const Component = InfoHoc(<div>jest div</div>)({ message: 'jest infoHoc message' });
const { getByTitle, findByDisplayValue } = render(Component);
const icon = getByTitle('jest infoHoc message');
act(() => {
fireEvent(
icon,
new MouseEvent('mouseover', {
bubbles: true,
}),
);
});
// Wait for the tooltip to show up
const tooltipText = await findByDisplayValue('jest infoHoc message')
expect(tooltipText).toBeInTheDocument();
});
Side note 1: I am not sure if you really need the act around fireEvent. testing-library should do it for you.
Side note 2: you can use user-event which has a cleaner syntax (and a .hover function)

Related

storybook mui 5 - listbox not found

I write test in storybook for my Select component. Which is basically styled Select from MUI ver. 5. I have trouble to access role="listbox" since it is outside root.
<div id="root"> Storybook render Select here</div>
<div role="presentation">
Here are my Select options
<ul role="listbox" ></u> - cannot acces this role after click. Unable to find an accessible element with the role "listbox"
<div>
This is my story.
const Template: ComponentStory<typeof StyledSelect> = (args) => <StyledSelect {...args} />;
export const ChangeOption = Template.bind({});
ChangeOption.args = {
labelId: 'dafault-select-label',
id: 'default-select',
label: 'select-under-test',
sx: { width: 240 },
value: 1,
children: [
<StyledMenuListItem key={'none1'} value={-1}>
None
</StyledMenuListItem>,
<StyledMenuListItem key={1} value={1}>
Option 1
</StyledMenuListItem>,
<StyledMenuListItem key={2} value={2}>
Option 2
</StyledMenuListItem>,
<Divider key="div3" />,
<StyledMenuListItem key={3} value={3}>
Option 3
</StyledMenuListItem>,
],
};
ChangeOption.play = async ({ canvasElement, globals }) => {
// Arrange
const canvas = within(canvasElement);
const selectElement = canvas.getByLabelText(/select-under-test/i);
await expect(selectElement).toHaveTextContent(/option 1/i);
// Act
await userEvent.click(selectElement);
const listbox = await canvas.getByRole('listbox');
// Unable to find an accessible element with the role "listbox"
};

How can I disable or change the href on a React-Leaflet v4 Popup close button?

In react-leaflet v4, the Popup component has a default href associated with the close button that directs to #close. Is there a way in React to modify this href or disable the href redirection? It's breaking my react-dom HashRouter.
Of note, Popup.js in Leaflet 1.8 has the following code:
var closeButton = this._closeButton = DomUtil.create('a', prefix + '-close-button', container);
closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
closeButton.setAttribute('aria-label', 'Close popup');
closeButton.href = '#close';
closeButton.innerHTML = '<span aria-hidden="true">×</span>';
The same issue is also in angular - means it seems to be the leaflet Lib:
// leaflet.js
close: function () {
if (this._map) {
this._map.removeLayer(this);
}
return this;
},
The close function has not even the $event as an argument and the "default" isn't prevented. This leaves us only dirty hacks:
Get the close button after the marker was displayed
Add a click handler more
Add a prefentDefault
yourMethodOpensTheToolTip(marker: Marker) {
if (marker && marker.openPopup) {
marker.openPopup();
// 1. get the close buttons, after the opened the popup
const closeButtons = document.getElementsByClassName('leaflet-popup-close-button');
// 2. add the event handler - if you have more than one, loop here
if (closeButtons && closeButtons.length > 0) {
L.DomEvent.on(closeButtons[0] as HTMLElement, 'click', function(ev){
ev.preventDefault(); // 3. stop it here
});
}
Just for reference the #close button as HTML:
Try something like this. It will probably disable any other hrefs that you may have in the popup though.
document.querySelector('.leaflet-pane.leaflet-popup-pane')!.addEventListener('click', event => {
event.preventDefault();
});
You can utilize useRef hooks and create a click event in the marker
const mapRef = useRef(null);
// event listener to handle marker click
const handleClick = () => {
mapRef.current._popup._closeButton.addEventListener('click', (event) => {
event.preventDefault();
})
};
const map = (<MapContainer center={position} zoom={13} scrollWheelZoom={false} style={{ height: '350px', width: '100%' }} ref={mapRef}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker
position={position}
eventHandlers={{
click: (e) => handleClick(),
}}
>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>)
if you are using GeoJSON you can use onEachFeature props
const onEachCountry = (country, layer) => {
const countryName = country.properties.ADMIN;
layer.on('click', function (e) {
layer
.bindPopup(countryName)
.openPopup()
._popup._closeButton.addEventListener('click', (event) => {
event.preventDefault();
});
});
};
const map = (<MapContainer style={{ height: '300px' }} zoom={1} center={[20, 100]}>
<GeoJSON style={countryStyle} data={mapData.features} onEachFeature={onEachCountry} />
</MapContainer>)
In my React project with react-leaflet v4, I had the same issue and I solved it with the "popupopen" event :
https://leafletjs.com/reference.html#marker-popupopen
<Marker
position={position}
eventHandlers={{
popupopen: (e) => {
e.popup._closeButton.removeAttribute("href");
e.popup._closeButton.style.cursor = "pointer";
}
}}
>
<Popup>
<p>Lorem ipsum dolor sit amet</p>
</Popup>
</Marker>
I hope it will help.
Building on Paul's answer. Here is the solution if you have multiple popups. This will handle the close button click event on all the popups that are open on the leaflet map.
// This is a stopgap till Leaflet fixes its issue with close buttons in popups in Leaflet maps
let popupCloseButtonsHTMLCollection = document.getElementsByClassName('leaflet-popup-close-button');
if(popupCloseButtonsHTMLCollection && popupCloseButtonsHTMLCollection.length > 0){
//convert the popupCloseButtonsHTMLCollection to array
var popupArray = [].slice.call(popupCloseButtonsHTMLCollection);
popupArray.forEach(button =>{
L.DomEvent.on(button as HTMLElement, 'click', function(ev){
ev.preventDefault();
});
});
}

Why the button inside the modal not working in Ionic?

I have created an ionic app. In the followig code, I have created a popover which some contents from json. When button clicked in popover it opens a modal which contain a button with onclick function.
But the function does not get called. I have tried same with two modal but still not working?
let courseNumber = 0;
interface ContainerProps {
courseNum: number,
onHide: () => void,
}
const PopoverList: React.FC<ContainerProps> = ({ courseNum, onHide }) => {
const [showTopicModal, setShowTopicModal] = useState(false);
return (
<><IonToolbar>
<IonTitle size="large">{courseJson[courseNumber]['title']}</IonTitle>
</IonToolbar><IonContent>
{courseJson[courseNumber]["content"].map((item) => <IonItemDivider key={item.toString()}>
<IonLabel>{item}</IonLabel>
</IonItemDivider>
)}
</IonContent>
<IonButton slot='bottom' size='default' color="success" expand='block' onClick={() => setShowTopicModal(true)}>Learn</IonButton>
<IonModal isOpen={showTopicModal} id="topic_modal">
<IonButton slot='bottom' size='default' color="success" expand='block' onClick={() => console.log("hello")}>Learn</IonButton>
</IonModal>
</>
)
}
I had the same problem. It turns out that after upgrading to Ionic 6 I forgot to wrap my routing in <IonApp></IonApp>. let me know if it helps.

Unable to display menu on right click react-big-calendar

I am trying to display menu on right click with react-big-calendar and material ui,
the issue that menu isnt display correctly on html its going on top right corner,
My code is:
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
return (
<>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={() => redirectToEvent(selectedEvent)}>
<ImportContactsTwoToneIcon
color="primary"
style={{ marginLeft: "15px" }}
/>{" "}
פתח אירוע
</MenuItem>
</Menu>
<Calendar
localizer={localizer}
events={events}
step={60}
views={["month", "day"]}
onSelectEvent={(event, e) => {
redirectToEvent(event);
}}
components={
{
eventWrapper: ({ event, children }) => (
<div
onContextMenu={
e => {
setSelectedEvent(event);
//think this is the issue
setAnchorEl(e);
e.preventDefault();
}
}
>
{children}
</div>
)
}
}
Material-UI has an example of providing a Context Menu, and it doesn't seem to use an anchorEl prop, or take the bare 'event' target object, placing a different object in state.
const handleContextMenu = (event) => {
event.preventDefault();
setContextMenu(
contextMenu === null
? {
mouseX: event.clientX - 2,
mouseY: event.clientY - 4,
}
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
// Other native context menus might behave different.
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
null,
);
};
const handleClose = () => {
setContextMenu(null);
};
From that part of the example, it would seem you need to update your onContextMenu accordingly. Since you're setting multiple state values for your menu, both for it's positioning and the referenced selectedEvent, you may want to use a reducer for state instead.
Then, on the <Menu> itself, it also mutates that state.
<Menu
open={contextMenu !== null}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
// menu items
</Menu>
And, since you've placed the <Menu> inside of your <Calendar>'s container object, what sort of styling is on your container and could that effect it's layout as well? (I don't know if Material-UI automatically portals it's menu in this scenario or not)

IonToggle firing twice in React-based Ionic project

In the following block of code, the IonToggle is firing twice for some unknown reason. I had it already replaced with a normal button and it works fine. If I keep the IonToggle and remove the line setUpdating(true) it also works fine.
Is it some known bug, or is there something wrong with this code.
import { AppContext } from './../AppContextProvider';
const LightController: React.FC<InterfaceLamp> = ({ id, color, brightness, turnedOn }) => {
const { state, dispatch } = useContext(AppContext);
const [isUpdating, setUpdating] = useState(false);
const isMount = useIsMount();
const handleUpdateToggle = async (isToggled: boolean) => {
lightService.toggleLight(state.api, id, isToggled, state.auth.username,
state.auth.password).then((res) => {
if (!res.error) {
[...]
dispatch({
key: 'devices',
data: devices,
})
}
setUpdating(false);
})
}
const handleToggle = (isToggled: boolean) => {
setUpdating(true);
handleUpdateToggle(isToggled);
}
return (
<div className="c-light">
<Loader isLoading={isUpdating} message={"Updating devices"} onClose={() => { }} />
<div className="c-light__controls">
<div className="c-light__toggle">
<IonItem lines={"none"}>
<button onClick={(e)=>handleToggle(!turnedOn)}>toggle</button>
<IonToggle checked={turnedOn} onIonChange={(e) => handleToggle(e.detail.checked)}/>
</IonItem>
</div>
</div>
</div>
);
};
export default LightController;
value="true"
why are you setting the value here? dont think it is necessary
As of Ionic React v5.5.0, IonToggle and IonCheckbox have still got the same issue.
The simplest workaround is to add an onClick event listener to the IonItem component that usually wraps the IonToggle component. (Alternately, use any other wrapper component with onClick.) This approach makes it possible to keep the native-looking IonToggle.