How do I simulate a value change on a Material UI Slider? - reactjs

I'm trying to test a state change via React Hooks (useState) by simulating a changed value on a Material UI slider, since that calls my hooks state update function. I'm then trying to verify the change by checking text that's displayed on the page. I feel like I've gotten close, no syntax errors yelling at me anymore, but it seems like the change isn't happening.
I've tried multiple different selectors and found that most people online are importing their Material UI element and using the imported item as the selector. I've tried this with and without .as(0), with a value of 500 as both a string and a number, and other variations.
My slider's onChange method directly calls my hooks state update method with the e.target.value to set as the new state value (and that all works fine in the actual app).
Any ideas of other things I can try? Or maybe there's another better way to test state changing with React hooks?
import ReactDOM from 'react-dom';
import App from './App';
import { createMount } from '#material-ui/core/test-utils';
import Slider from "#material-ui/core/Slider";
describe('App', () => {
let mount;
beforeEach(() => {
mount = createMount();
});
afterEach(() => {
mount.cleanUp();
});
it('updates the volts displayed when the slider value changes', () => {
const wrapper = mount(<App />);
wrapper.find(Slider).at(0).simulate('change', { target: { value: 500 } });
expect(wrapper.find('.volts-screen').text()).toEqual('0.500');
})
}```
ERROR
expect(received).toEqual(expected) // deep equality
Expected: "0.500"
Received: "0.000"

Ended up figuring it out. I imported the slider from material core (in the same way as in my component), and then I used the imported element in my wrapper.find and simulated a change with a second argument of { target: { value: 500 }}.
I thought this wasn't working at first, but realized that because in my case the effect was updating my component state which is async, I needed to add a setTimeout to make sure the update was captured before my check ran.
Saw some similar guidance on other Material UI elements, but didn't realize the slider would be so similar. Will leave this here just in case some other poor soul is scouring the internet for "Slider"-related info in particular ;)
//imported the element as below
import Slider from "#material-ui/core/Slider";
// found in wrapper using that element and simulated change with target value on timeout
it('updates the volts displayed when the slider value changes', () => {
const wrapper = mount(<App />);
wrapper.find(Slider).at(0).simulate('change', { target: { value: 500 } });
setTimeout(() => expect(wrapper.find('.volts-screen').text()).toEqual('0.500'), 0);
})

Just for general knowledge:
To set the value and interact not just with the component (i.e. the Context):
sliderToFind
.prop("children")[1]
.props.ownerState.onChangeCommitted(null, YOUR_VALUE);
sliderToFind.prop("children")[1].props.ownerState.value = YOUR_VALUE;
This is changing the value and firing the prop onChangeCommitted (or others i.e. onChange)

Related

How to implement promise when updating a state in functional React (when using useState hooks)

I know similar questions are bouncing around the web for quite some time but I still struggle to find a decision for my case.
Now I use functional React with hooks. What I need in this case is to set a state and AFTER the state was set THEN to start the next block of code, maybe like React with classes works:
this.setState({
someStateFlag: true
}, () => { // then:
this.someMethod(); // start this method AFTER someStateFlag was updated
});
Here I have created a playground sandbox that demonstrates the issue:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-6zss6q?file=/demo.tsx
Please push the button to get the confirmation dialog opened. Then confirm with "YES!" and notice the lag. This lag occurs because the loading data method starts before the close dialog flag in state was updated.
const fireTask = () => {
setOpen(false); // async
setResult(fetchHugeData()); // starts immediately
};
What I need to achieve is maybe something like using a promise:
const fireTask = () => {
setOpen(false).then(() => {
setResult(fetchHugeData());
});
};
Because the order in my case is important. I need to have dialog closed first (to avoid the lag) and then get the method fired.
And by the way, what would be your approach to implement a loading effect with MUI Backdrop and CircularProgress in this app?
The this.setState callback alternative for React hooks is basically the useEffect hook.
It is a "built-in" React hook which accepts a callback as it's first parameter and then runs it every time the value of any of it's dependencies changes.
The second argument for the hook is the array of dependencies.
Example:
import { useEffect } from 'react';
const fireTask = () => {
setOpen(false);
};
useEffect(() => {
if (open) {
return;
}
setResult(fetchHugeData());
}, [open]);
In other words, setResult would run every time the value of open changes,
and only after it has finished changing and a render has occurred.
We use a simple if statement to allow our code to run only when open is false.
Check the documentation for more info.
Here is how I managed to resolve the problem with additional dependency in state:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-gevciu?file=/demo.tsx
Thanks to all that helped.

Not simulating 'change' using enzyme

I have a Reactjs component and it has one button and a date-range picker.
I want to simulate onclick and onchange events of button and picker respectively.
I'm able to simulate onclick of the button. but on change of datepicker is not working
I have tried this
headerComponent.find(`#prev_button`).at(1).simulate("click");
headerComponent.find(`#dropdown`).at(1).simulate("change", { value: "t" });
please see this sandbox click here for full code and test file
Based on Enzyme documentation you make a mistake on your second argument on simulate function.
To simulate changes on the input, you should change it like this :
headerComponent.find(`#dropdown`).at(1).simulate("change", { target: { value: "t" } });
Testing with enzyme is tricky. You should try not to test dependencies because you trust those are already tested. Having said that, you could shallow render instead of mounting and look for the RangePicker component in the shallow tree, get the handler you are passing in the onChange prop and call it manually, then check the callback prop you pass to your component is called with the expected value.
describe.only("test", () => {
it("should render", () => {
const callBackToSetDates = jest.fn();
const callBackToSetFilter = jest.fn();
const wrapper = shallow(
<Header
{...headerProps1}
callBackToSetDates={callBackToSetDates}
callBackToSetFilter={callBackToSetFilter}
/>
);
const rangePickerOnChange = wrapper.find("RangePicker").prop("onChange");
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("someValue");
});
});
the purpose is to test only the logic you add inside your component, i.e., you transform the value you get from the RangePicker to something else
<RangePicker
...
onChange={(value) => {
callBackToSetDates(`I'm transforming ${value}`);
}}
/>
and in your test
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("I'm transforming someValue");
you can see it working here https://codesandbox.io/s/cool-rosalind-uec6t?file=/src/tests/index.test.js
If you really want to keep testing what the actual user sees, you'll need to fire the events that the user does when using the component. In this case: you need to click the input, look for a date, click it, then click another date to completely fire the onChange event of the RangePicker component. You might look at how antd test it and copy the necessary jest configuration they have to mock some DOM APIs

Firing a click event on a child component

I have been trying to write a test that will ensure that when a marker is clicked (from leaflet) further details will be displayed to the user. The Marker component is a child of the Map component. To start I am just wanting to see if the onClick function is called once when the marker is clicked.
The Map component returns the following structure
return(
<LeafletMap>
<Marker data-testid='marker' onClick={someFunc}/>
<TileLayer/>
<Popup/>
</LeafletMap>
)
In my test I attempt to render the Map component and find the marker via a data-testid:
const handleParcelClick = jest.fn()
it('get parcel details upon clicking the marker', () => {
const {getByTestId}= render(<Map lat={someNumber} lng={someNumber} zoom={14} parcels={fakeParcels} activeParcel={fakeDetails} onParcelClick={handleParcelClick} />)
const marker = getByTestId('marker')
fireEvent.click(marker)
expect(handleParcelClick).toBeCalledTimes(1)
});
When attempting to run I get the following error:
at getElementError (node_modules/#testing-library/dom/dist/query-helpers.js:22:10)
at args (node_modules/#testing-library/dom/dist/query-helpers.js:76:13)
at getByTestId (node_modules/#testing-library/dom/dist/query-helpers.js:59:17)
at Object.<anonymous>.it (src/ParcelDetails.test.tsx:58:20)
I have attempted using enzyme as well with no success. The data-testid in the actual code is unique for each marker, called marker above for simplicity. Am I going about this wrong? Should I be testing the Marker separately from the Map component?
Update: I have attempted to use enzyme as a solution; however, I receive the following error when trying to simulate a click
TypeError: Cannot read property '__reactInternalInstance$k2volvgmsgj' of null
There does not seem to be a consistent solution for this error and I am confused as to why I am getting it. I have ensured that marker is the component I am wanting to click and that it is not null.
Here is my updated code:
it('Loads parcel details on click', ()=> {
const mockClick = jest.fn();
const component = mount(<Map lat={n1} lng={n2} zoom={14} parcels={fakeParcels} activeParcel={fakeDetails} onParcelClick={mockClick} />);
const marker = component.find(Marker).first();
marker.simulate('click');
expect(mockClick).toBeCalledTimes(1);
});
The easiest way to fire a click event on an element in jest is to first find the element using dom selector and then simulate click on it, like this:
let element = document.getElementById('your-element-id');
element.simulate('click');
Hope this helps!!
I was able to get the desired behaviour via enzyme. Although it is not the best solution - it will do for now. I know shallow rendering is not the best practice.
Here is a snippet of my solution using shallow from enzyme:
it('Loads parcel details on click', ()=> {
const onParcelClick = jest.fn();
const component = shallow(<Map lat={n1} lng={n2} zoom={14} parcels={mockParcels} activeParcel={mockDetails} onParcelClick={onParcelClick} />);
const marker = component.find(Marker).first();
marker.simulate('click');
expect(onParcelClick).toBeCalledTimes(1);
});

How do I test a method defined within a functional component, that interacts with DOM elements and has no arguments

I have been having trouble getting 100% test coverage on one of my buttons (A React functional components.) Basically when it is clicked, it executes some code and then also calls another method from within this onClick called resetButtons. This method will find all the buttons like it in the app and remove a class. This is a preemptive behavior so that only one button at a time can be active.
So far I have tested the click using .simulate, passing in a mocked domElement. And then test that the domElement.classList.add method is called with 'active'.
Obviously this being a DOM centered operation, I am finding it very difficult to test the resetButtons method that lies within the component. especially considering it doesn't have any methods.
I have tried defining the resetButtons method outside of the component and then exported it so the jest test could import it. However I have been unable to test the method as it seems to want it to be a spy or mock, and not the method itself. (Matcher error: received value must be a mock or spy function
)
Here is the react Functional Component:
import React from 'react';
import PropTypes from 'prop-types';
import classes from './MainButton.module.scss';
const MainButton = (props) => {
const resetButtons = () => {
const elements = document.getElementsByClassName('mainButton');
for (let i = 0; i < elements.length; i += 1) {
elements[i].classList.remove('active');
}
};
const handleClick = (event) => {
if (!event.target.classList.contains('active')) {
resetButtons();
event.target.classList.add('active');
props.setVisualState(props.className.split('-')[0]);
}
};
return (
<button
onClick={handleClick}
type="button"
className={`${classes.mainButton} ${props.className}`}
>
{props.children}
</button>
);
};
MainButton.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
setVisualState: PropTypes.func.isRequired,
};
MainButton.defaultProps = {
children: 'Button',
className: '',
};
export default MainButton;
Here is the Test
import React from 'react';
import { shallow } from 'enzyme';
import MainButton from './MainButton';
describe('MainButton', () => {
const domElement = { classList: { contains: jest.fn(), remove: jest.fn(), add: jest.fn() } };
const setVisualStateMock = jest.fn();
const mainButton = shallow(<MainButton setVisualState={setVisualStateMock} />);
it(' is rendered properly', () => {
expect(mainButton).toMatchSnapshot();
});
describe('when clicked', () => {
beforeEach(() => {
mainButton.find('button').simulate('click', { target: domElement });
});
it('it runs `classlist.add` to assign `active` class', () => {
expect(domElement.classList.add).toHaveBeenCalledWith('active');
});
it('it runs set visual state to update `Allergen` container `state`', () => {
expect(setVisualStateMock).toHaveBeenCalled();
});
});
});
Currently the coverage report is reporting 92% coverage, but the branch is at 50 and the line that is causing the trouble is on line 9 (the elements[i].classList.remove('active'); line.
I know at 90% I should probably just move on but this is something I want to be able to figure out. Feel like getting head around this will make me a better tested.
Hope you guys can help!
Fumbling around in the DOM yourself is an anti-pattern. That's React's job. Instead of manipulating the dom with target.classList.add you should have a state property that holds the status which of your inputs is currently active. Then, while rendering you can say className={isActiveInput ? "active": null}.
Because the state is not specific to your MainButton component you would lift the state up. If you have the state somewhere in the parent you don't have to crudely search for DOM elements by classname and manipulate the dom yourself.
Simply put, the rule of React is: You define how things are supposed to look like, React takes care that your definition becomes reality in the dom. If you manipulate the DOM yourself - you're doing it wrong.
When all of this is done, you will have no problem at all with tests, because all you have to do is provide the proper state and props, which is easy, and check that your callback is triggered onClick.
EDIT: Advanced version would be to use Context, but I'd go with state lifting first.
You should be able to mount multiple MainButtons, click one and expect that the other(s) had domElement.classList.remove called on them.
However, user konqi is right in that React provides better ways of manipulating elements/components.
You could replace this test:
expect(domElement.classList.add).toHaveBeenCalledWith('active');
with a test that checks that the button has (or does not have) the active className (instead of checking that the function was called with the right argument). With that test in place, if you like, you could refactor this in the way that konqi suggests.

How to create an unit test for UncontrolledTooltip from reactstrap that does not handle state management directly?

I implemented simple UncontrolledTooltip from reactstrap. The doc (https://reactstrap.github.io/components/tooltips/) says
uncontrolled component can provide the functionality wanted without the need to manage/control the state of the component
If I want to implement an unit test (e.g. jest + enzyme) for testing its state as either open or close, how can I create a unit test without manually tinkering with state value? Is this possible to achieve it? It seems only possible with regular Tooltip component but I like to hear advice from seasoned engineers.
[Update]:
Upon request I include here tooltip and unit test I am trying to execute. At the moment, I want to simulate hover on the tooltip however mockHover.mock.calls.length returns as 0 which I interpret as mock function was not triggered.
Here is my Tooltip.
import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
export default class MyTooltip extends React.Component {
render() {
const { metaData, wg } = this.props;
return (
<div>
<UncontrolledTooltip placement="bottom" trigger={'hover'} target={wg}>
{metaData}
</UncontrolledTooltip>
</div>
);
}
}
Here is my unit test that use jest and enzyme:
describe('<MyTooltip />', () => {
it('Tooltip unit test', () => {
const mockHover = jest.fn();
const wrapper = shallow(<MyTooltip trigger={mockHover} />);
expect(wrapper.find(UncontrolledTooltip));
wrapper.find(UncontrolledTooltip).simulate('hover');
expect(mockHover.mock.calls.length).toEqual(1);
});
});
There are few important things to start from:
UncontrolledTooltip is part of 3rd party package so you won't test it explicitly.
Instead you better focus on testing your wrapper around UncontrolledTooltip.
simulate is nothing related to events browser's system. It's just a syntax sugar to do props().onHover(...). So if target component has such a prop - and it's a callback-function - it will be called. If there is no such a prop - it would be up to defaultProps what's going on. Anyway nothing like 'emulating mouse cursor over the element'.
shallow() will stop rendering at level of UncontrolledTooltip(its internals will not be rendered)
Keeping that in mind I see you able only:
your component finally renders UncontrolledTooltip with expected constant prop values
both metaData and wg props are passed down to UncontrolledTooltip
it('renders UncontrolledTooltips under the hood', () => {
const wg = '1';
const metaData = (<span>2</span>);
const wrapper = shallow(<MyTooltip wg={wg} metaData={metaData} />);
const innerTooltip = wrapper.find(UncontrolledTooltip);
/*
I don't validate `find(UncontrolledTooltip).toHaveLength(1)`
since assertion on `.find(..).props()` would throw exception otherwise
*/
expect(innerTooltip.props().placement).toEqual('bottom');
expect(innerTooltip.props().trigger).toEqual('hover');
expect(innerTooltip.props().wg).toEqual(wg);
expect(innerTooltip.props().metaData).toEqual(metaData);
});

Resources