I am trying to write a test for a simple Material-UI Textfield select component. The test should show that selecting an options triggers the corresponding event.
Here ist the component
<TextField
inputProps ={{"data-testid": "testId"}}
id="TextFieldId"
aria-label={"TextFieldAriaLabel"}
select
label="Files"
value={limit}
onChange={handleLimitChange}
SelectProps={{
native: true,
}}
variant="outlined"
>
{[{value: 5, label: "5"}, {value: 10, label: "10"}, {value: 15, label: "15"}].map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</TextField>
Selecting the value "10" triggers the call of a function with 10 as an input parameter.
I use jest to write the test. I want to click on the select component to open the dropdown. Then I want to click on one of the options. To prove that the event was triggered, I check if the related function is called with the right argument.
It sound very simple, but I ran into many issues. Most of the issues were related to not finding the elements, since material ui nests different html components. My so far best approach looks like this.
testObject.renderResult.getByTestId('testId').click();
testObject.rerender();
jest.runAllTimers();
const dropdown = testObject.renderResult.getByTestId('testId');
within(dropdown).getByText('10').click();
testObject.rerender();
jest.runAllTimers();
expect(mostRecentImports).toHaveBeenCalledWith(10)
Jest finds the elements, but the test fails. The component remains in its default, showing the value 5 (not 10). What am I doing wrong?
I also had the same problem in my app. Finally I solved it using this solution I found here: https://github.com/testing-library/react-testing-library/issues/322#issuecomment-581650108
Write a simple function that opens the select menu and selects the wanted option:
const selectMaterialUiSelectOption = (element, optionText) => {
// The the button that opens the dropdown, which is a sibling of the input
const selectButton = element.parentNode.querySelector('[role=button]');
// Open the select dropdown
UserEvent.click(selectButton);
// Get the dropdown element. We don't use getByRole() because it includes <select>s too.
const listbox = document.body.querySelector('ul[role=listbox]');
// Click the list item
const listItem = within(listbox).getByText(optionText);
UserEvent.click(listItem);
};
[... in your test]
selectMaterialUISelectOption(getByTestId('testId'), "10")
You can utilize the findAByDisplayValue method of #testing-library/react
https://testing-library.com/docs/dom-testing-library/api-queries#bydisplayvalue
Related
I'm creating a React form with Material UI. My goal is to force the user to answer all the questions before they can click the download button. In other words I'm trying to leave the download button in the disabled state until I can determine that values are set on each field. I've tried to get this working with and without react-hook-form.
What I've tried
Without react-hook-form...
I have my example in coding sandbox here:
https://codesandbox.io/s/enable-download-button-xwois4?file=/src/App.js
In this attempt I abandoned react-hook-form and added some logic that executes onChange. It looks through each of the formValues and ensures none of them are empty or set to "no answer". Like this:
const handleInputChange = (e) => {
// do setFormValues stuff first
// check that every question has been answered and enable / disable the download button
let disableDownload = false;
Object.values(formValues).forEach((val) => {
if (
typeof val === "undefined" ||
val === null ||
val === "no answer" ||
val === ""
) {
disableDownload = true;
}
});
setBtnDisabled(disableDownload);
The problem with this approach, as you'll see from playing with the UI in codesandbox, is that it requires an extra toggle of the form field value in order to detect that every field has been set. After the extra "toggle" the button does indeed re-enable. Maybe I could change this to onBlur but overall I feel like this approach isn't the right way to do it.
Using react-hook-form
With this approach...the approach I prefer to get working but really struggled with, I ran into several problems.
First the setup:
I removed the logic for setBtnDisabled() in the handleInputChange function.
I tried following the example on the react-hook-form website for material ui but in that example they're explicitly defining the defaultValues in useForm where-as mine come from useEffect. I want my initial values to come from my questionsObject so I don't think I want to get rid of useEffect.
I couldn't figure out what to do with {...field} as in the linked material ui example. I tried dropping it on RadioGroup
<Controller
name="RadioGroup"
control={control}
rules={{ required: true }}
render={({ field }) => (
<RadioGroup
questiontype={question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
{...field}
>
but I get an MUI error of : MUI: A component is changing the uncontrolled value state of RadioGroup to be controlled.
Also, I don't see that useForm's state is changing at all. For example, I was hoping the list of touchedfields would increase as I clicked radio buttons but it isn't. I read that I should pass formState into useEffect() like this:
useEffect(() => {
const outputObj = {};
Object.keys(questionsObject).map(
(question) => (outputObj[question] = questionsObject[question].value)
);
setFormValues(outputObj);
}, [formState]);
but that makes me question what I need to do with formValues. Wondering if formState is supposed to replace useState for this.
I have a select:
<Select classNamePrefix="select" ref={myRef} menuPortalTarget={document.body} styles={style} placeholder="Select Foods" name="Foods" value={inputField.foods} options={options} onChange={event => handleInputChange2(index, event)} className="select selectNarrow" />
const handleInputChange2 = (index, event) => {
const values = [...inputFields];
values[index] = event;
setInputFields(values);
console.log(event);
};
Whereby {options} are built from a collection. On this same page I can add to this collection - after adding a new item and then opening the select it is not immediately visible until after I have chosen an already existing item. How do I get the React-Select to refresh the list each time I open it (rather than an onChange)
I feel like we're missing some code to properly answer your question. How did you add a 'new item'? While you added to the collection, did you also update your options array?
Displayed options are controlled by the options prop. If you add a new option to this array, React-Select will rerender and the new option would be available.
I am new to react and office-ui-fabric and I am trying to create a VSCode Extension takes certain input parameters and based on the input the user selects, I should be operating on the option selected. I take the user input in the form of options under dropdown button. Currently once the user selects one of the options from the dropdown i do not see the option selected in the dropdown.I see only undefined being displayed in the console.
I have tried different ways of getting this input parameters from the user but i still end up with undefined being show in the console.
const Options: IDropdownOption[] = [
{ key: 'A', text: 'A', itemType: DropdownMenuItemType.Header },
{ key: 'B', text: 'B' },
{ key: 'C', text: 'C' },
];
const [selectedItem, setOption] = React.useState<string | undefined>(undefined);
return (
<div>
<Dropdown placeholder="Select option" options={Options} styles={dropdownStyles} selectedKey={selectedItem} onChange={event =>{
setOption((event.target as HTMLInputElement).value);console.log(selectedItem)}} />
)
windows.console should show me the input the user selects whereas currently i see undefined
The office-ui-fabric Dropdown component triggers change events via the onChanged prop which when invoked, passes the actual option object underlying the drop down item that has been selected by the user.
By changing onChange to onChanged, and then revising this events handler to read the text of the selected option (rather than value of the event.target) that should produce the required result:
<Dropdown placeholder="Select option"
options={Options}
styles={dropdownStyles}
selectedKey={selectedItem}
onChanged={ selectedOption =>{
setOption(selectedOption.text);
console.log("Selected", selectedOption.text);
}}/>
Update
This: setOption(selectedOption.text); calls the setOption() function, and passes the display value of the option selected by the user to it. When calling setOption(), this updates the stat selectedItem state of the component with the value that is passed (ie the display value of the selected option). Note that a side effect of calling setOption() is that a re-render of the component will occour, meaning that parts of your component displaying/relying on selectedItem will be automatically updated, etc.
I'm using MaterialUI components and i map through an array of object to generate some checkboxes as shown below.
const wrapper = () => {
...
return(
<FormGroup>
{Object.keys(products).map((key) => {
return <FormControlLabel label={products[key].name} key={key} control={
<CheckBox value={products[key].name} />
}
})
);
}
So given the code above. Let's say the products array has 3 object. Whenever i check a checkbox, i want all others to get a checked false and the one i checked a check true.
I'm using the state Hook so the code above is a functional component.
Material-UI Checkbox Component takes a prop checked of boolean type.
What you can do is maintain the flag for each checkbox and whenever any checkbox if pressed, just make its value in state true, and false the other and pass that state checked prop of each checkbox.
BTW, the behavior you want to achieve here is not done by checkboxes, it is the behavior of Radio Button, where the user can select only one option at a time while checkboxes are used for multiple selections.
(I believe material-ui checkbox documentation has a very clear explanation on this, Let me know if you can keep up by yourself or you want sample code too.)
I would suggest using Radio buttons instead of checkBoxes in this case.
https://material-ui.com/components/radio-buttons/
No more than one radio button can be selected at a time which is what you are describing.
How can we display the selected value just below the input box.
Use Case:
We are using multiple select of react-select , when we select the value from the select box , it comes inside the input box as selected. Can we have a method or something to get the selected values outside the input box (just below it)
Thanks in Advance!
I had a similar problem and I solved it creating a wrapper of react-select component and adding a state to my custom component. When the react-select changes I added the selected items to my component state and I show the in a custom div below, there you can add the styles that you want. Here an example of my approach: https://codesandbox.io/s/2kyy4998y
Hope this helps you.
Regards
I recently had to do this for a project I'm working on and wrote up how I did it here. The gist is that you need a wrapper component
// SelectWrapper.js
import ReactSelect from 'react-select'
const SelectWrapper = (props) => {
const { isMulti, value } = props;
return (
<div>
{isMulti ? value.map((val) => <span>{val.label}</span>) : null}
<Select {...props} controlShouldRenderValue={!isMulti} />
</div>
)
}
The very important part here is the controlShouldRenderValue prop which we disable when isMulti is true so the select dosn't show any selected values instead letting us take care of that