Select a item from a mui component listbox in cypress - reactjs

This is the element.
<input aria-invalid="false" autocomplete="off" placeholder="Category" type="text" class="MuiOutlinedInput-input MuiInputBase-input MuiInputBase-inputAdornedEnd MuiAutocomplete-input MuiAutocomplete-inputFocused MuiAutocomplete-input MuiAutocomplete-inputFocused css-16sx77j" aria-autocomplete="list" autocapitalize="none" spellcheck="false" value="" id="mui-621338585" aria-controls="mui-621338585-listbox" aria-activedescendant="mui-621338585-option-3"> So this is a listbox by name Category, that contains various options when I click the dropdown arrow. The number after "mui-" is dynamic.
This is what I tried:
cy.get('[id^=”mui-"]').eq(2);
Also tried:
cy.get('[id^=”mui-"]')
.find('[aria-activedescendant*="-option-"]').eq(2);
And tried:
cy.get('[aria-activedescendant*="-option-2"]');
Could someone point me in the right direction to select an option from the listbox? Appreciate your help.

Don't over-think the problem, just use text in the component.
Libraries like React Material-UI generate quite complicated HTML to handle styling and animation and it's difficult to pick the right parts from that generated HTML.
Given source code like this (taken from MUI demo page)
<Autocomplete
disablePortal
id="combo-box-demo"
options={top100Films}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
You get a generated structure like this (leaving out classes used for styling)
<div class="MuiAutocomplete-root" data-cy="movie-autocomplete">
<div>
<label for="combo-box-demo" id="combo-box-demo-label">Movie</label>
<div>
<input id="combo-box-demo" type="text" role="combobox" value="">
...
</div>
</div>
</div>
The MUIAutocomplete-root is outer element - you can test it using the text in the elements.
cy.contains('.MuiAutocomplete-root', 'Movie') // identify Autocomplete component
.click() // open it
cy.contains('The Godfather').click() // choose an option
cy.contains('.MuiAutocomplete-root', 'Movie')
.find('input')
.should('have.value', 'The Godfather') // verify the value in the input
With data-cy attribute
If you add a data-cy attribute to the component,
<Autocomplete
data-cy='movie-autocomplete'
disablePortal
id="combo-box-demo"
options={top100Films}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
The test becomes simpler
cy.get('[data-cy="movie-autocomplete"]').click()
cy.contains('The Godfather').click()
cy.get('[data-cy="movie-autocomplete"]')
.find('input')
.should('have.value', 'The Godfather')

Related

Error using AutoComplete of Material UI + react hook form

I am using the component 'AutoComplete' of material Ui to render multiple Checkboxes, and show the options selected into a TextField.
The error occurs when I submit the form. The values of checkboxes selected are empty, like this: category: ""
It seems the react hook form is not recognizing the name "category", like below:
<Autocomplete
id="checkboxes-tags-demo"
fullWidth
multiple
limitTags={2}
getOptionLabel={(option) => option.title}
disableCloseOnSelect
noOptionsText="Nenhuma opção foi encontrada"
variant="outlined"
options={newCategories}
renderOption={(option, {selected}) => {
return (
<Box key={option.id} ml={option?.isSub ? 3 : 0}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
{option.title}
</Box>
)
}
}
renderInput={(params) =>
<TextField
name="category"
inputRef={register}
{...params}
label="Selecione a categoria"
variant="outlined" />}
/>
}
/>
You need to wrap the Material UI Autocomplete with the Controller Component provided by React Hook Form. See this section in the documentation for further information.

How do I use the SpeedDial to upload a file?

(this seem to have been asked previously but I couldn't find any hint on if it was actually answered)
MUI has a good demo for creating upload buttons which boils down to:
<input accept="image/*" className={classes.input} id="icon-button-file" type="file" />
<label htmlFor="icon-button-file">
<IconButton color="primary" aria-label="upload picture" component="span">
<PhotoCamera />
</IconButton>
</label>
What I wonder is how to implement the same using the Speed Dial. Inherently the SpeedDialAction seems to materialize as a <button/>, but it's not possible to e.g. wrap the SpeedDialAction in a <label htmlFor /> as its parent will try to set some props on it and will fail.
So how do I initiate the file selection from within the Speed Dial or a FAB in general?
You can create a wrapper component that forwards props to SpeedDialAction.
function UploadSpeedDialAction(props) {
return (
<React.Fragment>
<input
accept="image/*"
style={{ display: "none" }}
id="icon-button-file"
type="file"
/>
<label htmlFor="icon-button-file">
<SpeedDialAction
icon={<CloudUploadIcon />}
tooltipTitle="upload"
component="span"
{...props}
></SpeedDialAction>
</label>
</React.Fragment>
);
}
https://codesandbox.io/s/material-demo-forked-h6s4l
(Note to future readers: For v5, time allowing, we hope to rationalise where props rather than context are used to control children, in order to solve exactly this kind of issue. So check whether this solution is still needed.)
It is - in my knowledge - not possible to add the htmlFor in any way. So what I would do is to add a hidden input type file and then add a ref to it. Then in the onclick of the SpeedDialAction button I would call a handler function that clicks on the input ref. Like this:
const inputRef = useRef();
const handleFileUploadClick = () => {
inputRef.current.click();
};
Then your SpeedDialAction:
<SpeedDialAction
onClick={handleFileUploadClick}
... the rest of your props
/>
And then finnaly your actual input:
<input
style={{ display: "none" }}
ref={inputRef}
accept="image/*"
id="contained-button-file"
multiple
type="file"
/>
Working demo: https://codesandbox.io/s/material-demo-forked-f9i6q?file=/demo.tsx:1691-1868

React material UI autocomplete is not working with the formik

I have this code snippet, which is written by someone else.
<FormControl className={classes.formControl}>
<InputLabel id="combo-box-demo">
{values.type === "forAllCustomers" ? "" : ""}
</InputLabel>
<Autocomplete
id="combo-box-demo"
name="customerId"
onBlur={handleBlur}
onChange={handleChange}
value={values.customerId}
options={agents}
getOptionLabel={(option) => option.name}
disabled={values.type === "forAllCustomers"}
renderTags={(value, getTagProps) => {
filteredAgents(values.type).map(
(option, agentId) => (
<Chip
variant="outlined"
label={option.name}
// size="small"
{...getTagProps({ agentId })}
/>
)
);
}}
renderInput={(params) => (
<TextF
{...params}
variant="outlined"
label="Customer"
placeholder="Select"
name="agentId"
/>
)}
/>
</FormControl>
Here we load bunch of agents. If user pick one agent, that agents id should set as the customerId.
Here we use formik, so onBlur={handleBlur} onChange={handleChange} is controlled by the formik.
I tried by setting value to values.customerId But it seems not working and also I am getting an error in the console saying
index.js:1 Material-UI: The getOptionLabel method of Autocomplete
returned undefined instead of a string for "".
How do I fix this issue?
Anyhelp!
Thanks in advance. =)
See, the signature of the function onChange of AutoComplete is:
function(event: object, value: T | T[], reason: string) => void
However, signature of handleChange of Formik is
handleChange: (e: React.ChangeEvent<any>) => void
The problem is that simply passing onChange={handleChange} will not do what you think.
See, if you put, before the return statement a console.log(values), you'll see your initialValues object. However, a change in the Autocomplete will fill this object with strange combo-box-demo-option-0 1, 2 and so on. This is because how the Autocomplete component handles the combobox and the name and id properties. According to Formik, handleChange will look for the name or id to operate, and none have the correspondence you want.
Enough said, to fix your problem, you have to use another method provided by Formik: setFieldValue
Your Autocomplete should look something on the lines of:
<Autocomplete
id="combo-box-demo"
name="customerId"
onChange={(e, v) => {
setFieldValue("name", v?.name || "");
setFieldValue("customerId", v?.id || "");
}}
value={values}
options={agents}
getOptionLabel={(option) => option.name || ''}
style={{ width: 300 }}
renderInput={(params) => (
<TextField {...params} label="Combo box" variant="outlined" />
)}
/>;
You will not need the useState because you are not handling yourself any state changes. A regular javascript object will be enough.
Please check the agents you are getting. There might be an issue with your agents data coming from API RESPONSE or from anywhere else. According to Material-UI, the parameter you pass to options should be in an Array but your one might be an Object.
Please convert the Data type of agents to Array instead of Object if it is not in an Array and it will work!
<Autocomplete
id="combo-box-demo"
name="customerId"
onBlur={handleBlur}
onChange={handleChange}
value={values.customerId}
options={agents} //This should be in An Array
getOptionLabel={(option) => option.name} //Then access name
disabled={values.type === "forAllCustomers"}
/>
Please check the Official Docs of Material-UI https://material-ui.com/components/autocomplete/

How to make autocomplete field of material UI required?

I have tried a couple of ways in order to make the material UI's autocomplete field of type required but I am not getting the behavior that I wanted. I had encapsulated my field inside react hook form <Controller/> yet no luck. I want to trigger message 'Field is mandatory' on submit when nothing is added to the field.
Below is the code snippet, I have not removed comments so that it becomes a bit easier for others to understand the approach that I had followed earlier -
<Controller
name="displayName"
as={
<Autocomplete
value={lists}
multiple
fullWidth
size="small"
limitTags={1}
id="multiple-limit-lists"
options={moduleList}
getOptionLabel={(option) => option.displayName}
renderInput={(params,props) => {
return (
<div>
<div className="container">
<TextValidator {...params} variant="outlined" label="Display Name*" className="Display Text"
name="displayName" id="outlined-multiline-static"
placeholder="Enter Display-Name" size="small"
onChange={handleDisplay}
// validators={['required']} this and below line does throw a validation but the problem is this validation stays on the screen when user selects something in the autocomplete field which is wrong.
// errorMessages={['This field is required']}
// withRequiredValidator
/>
</div>
</div>
)
}}
/>
}
// onChange={handleDisplay}
control={control}
rules={{ required: true }}
// required
// defaultValue={options[0]}
/>
<ErrorMessage errors={errors} name="displayName" message="This is required" />
You can use the following logic to get it worked. Though this might not be the best solution but works.
<Autocomplete
renderInput={(params) => (
<TextField
{...params}
label={value.length === 0 ? title : title + " *"} //handle required mark(*) on label
required={value.length === 0}
/>
)}
/>
I tried using the built in required in textfield for autocomplete, and it works like a charm. Maybe you can use this as a reference.
<Autocomplete
renderInput={(params) => {
<TextField {...params} required />
}
// Other codes
/>
Since you are rendering <TextValidator>, you should apply mandatory(required) attribute to that component not on <AutomComplete>.
Try this if your Material UI version is v5
<TextField
{...params}
required
label="Tags"
value={value}
InputProps={{
...params.InputProps,
required: value.length === 0,
}}
/>

Material UI : React Autocomplete component (controlled) and disableCloseOnSelect

I have a problem using the Autocomplete component provided by Material UI for React.js.
Here is what my component looks like :
<Autocomplete
options={options}
value={value}
disableCloseOnSelect
onChange={handleChange}
limitTags={4}
getOptionLabel={(option) => option.name }
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
className={classes.checkbox}
checked={selected}
/>
{option.name}
</React.Fragment>
)}
renderInput={(params) => (
<TextField {...params} variant="outlined" label={label} placeholder={label} />
)}
{...custom}
/>
I can't get the feature disableCloseOnSelect to function with a controlled component, as it does not prevent the list from closing after select... If I remove the props value and onChange everything works perfectly, but I need them for my project.
Does anybody have the same problem or see anything wrong with the way I am handling things ?
There is an issue that seems to match mine here. In that case, could it be a bug ?
This is most probably caused by the mother component re-rendering. Autocomplete's state needs to be localised.
Here's a sandbox demonstration of the difference this makes:
https://codesandbox.io/s/priceless-wilson-zz59q?file=/src/App.js
It also has performance benefits.

Resources