Testing a Material ui Select component without using testid - javascript

I am currently using Material UI and react-testing-library on a form. I have a Select which is populated by an array of objects.
import React, { useState } from 'react';
import { Select, MenuItem, FormControl, InputLabel } from '#material-ui/core';
const options = [
{
label: 'Example 1',
value: 123456789,
},
{
label: 'Example 2',
value: 987654321,
},
];
const SelectTesting = () => {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<FormControl variant="outlined" fullWidth>
<InputLabel id="selectedOptionLabel">Select an Option</InputLabel>
<Select
labelId="selectedOptionLabel"
id="selectedOption"
value={value}
onChange={handleChange}
label="Select an Option"
>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default SelectTesting;
In my testing file what I want to be able to do is check if the actual value of the select is correct. I can always just check the label but in this scenario the value is what is import. So if I select Example 1. I want to run a test to check to see if the selectedOption input has a value of 123456789
import React from 'react';
import { screen, render } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SelectTesting from '.';
describe('SelectComponent', () => {
it('value should be 123456789', async () => {
render(<SelectTesting />);
const select = await screen.findByLabelText(/^select an option/i);
userEvent.click(select);
const option = await screen.findByRole('option', { name: /^example 1/i });
userEvent.click(option);
// want to check to see if value is 123456789
});
});
I have seen some questions where people say use the data-testId prop but I don't really want to use that since it's the last thing react-testing-library recommends to use.
I also don't want to use Enzyme as it isn't supported with React 18 anymore

Using Enzyme, Jest & Material UI V4/V5
const wrapper = mount(<SelectTesting />);
const select = wrapper.find({ id: [select_id], role: "button" }).hostNodes();
select.simulate("mouseDown", { button: 0 });
const option = wrapper.find({id: [option_id] }).hostNodes();
option.simulate("click");

Related

ReactJS Hooks - investigation code's 'Objects are not valid as a React child' error and JS interpretation of a variable to an array

So, I have been attempting to read about this error ( Objects are not valid as a React child. If you meant to render a collection of children, use an array instead and here https://www.g2i.co/blog/understanding-the-objects-are-not-valid-as-a-react-child-error-in-react ), and I (think I) understand it's an issue of passing a 'complex' object to react's child component/passing a JS object to a component. (am I correct?)
I have located where my issue lays in my code, but I cannot really understand why would the variable be interpreted as a complex object in the first place.
The issue occurs when I'm trying to create at TodoApp.js, addToDo() function.
This function is later on transferred to a child component 'TodoForm.js', so it could, on handleSubmit, call his father's function addToDo():
In addToDo(), if I edit, where I add a new element, the field txt 's value to a string for example, it works.
TodoApp.js (in code's comments I have pointed out where is exactly the issue)
import React, {useState} from "react";
import TodoList from './TodoList';
import Paper from '#mui/material/Paper';
import List from '#mui/material/List';
import TextField from '#mui/material/TextField';
import ListItem from '#mui/material/ListItem';
import Divider from '#mui/material/Divider';
import ListItemText from '#mui/material/ListItemText';
import { AppBar, Toolbar, Typography } from "#mui/material";
import { fontWeight } from "#mui/system";
import TodoForm from "./TodoForm";
export default function () {
let tasks = [
{id: 1, txt: "thisisTask1", completed: true},
{id: 2, txt: "thisisITask2", completed: false},
{id: 3, txt: "SO ORIGINAL", completed: false}
]
let [tasksVal, tasksEdit] = useState(tasks);
const tasksTxts = tasksVal.map((task) =>
<><ListItem><ListItemText>{task.txt}</ListItemText></ListItem> <Divider/></>
)
//issue seems to lay here - if I change txt: "texthere" - no errors (but doesn't add the task either)
let addToDo = (taskTxt) => { // a function to later on send to TodoForm for when a Submit occures.
tasksEdit([...tasksVal, { id: 4, txt: {taskTxt}, completed: false }])
};
return(
<div>
<Paper style={{padding: 0, margin: 0, height: "100vh", backgroundColor: "whitesmoke"}}>
<AppBar position='static' style={{height: "64px"}} color="primary">
<Toolbar>
<Typography style={{letterSpacing: "3px", fontSize: "40px"}}>Todos with Hooks!</Typography>
</Toolbar>
</AppBar>
<TodoList tasksTxts={tasksTxts}/>
<TodoForm AddToDo={addToDo}/>
</Paper>
</div>
)
}
TodoForm.js
import React from "react";
import useInputStatee from "./hooks/useInputStatee";
import { Paper, TextField } from "#mui/material";
export default function ({AddToDo}) {
const [inputVal, inputChange, inputReset] = useInputStatee("");
console.log(inputVal)
return (
<div>
<Paper>
<form onSubmit={e => {
AddToDo(inputVal);
// inputReset();
}}>
<TextField value={inputVal} onChange={inputChange}/>
</form>
<p>{inputVal}</p>
</Paper>
</div>
)
}
useInputStatee.js
import React, {useState} from "react";
export default function (initVal) {
let [value, setValue] = useState(initVal);
let handleChange = (e) => { // when we get from a form ... we get an e.target.value
setValue(e.target.value);
}
let reset = () => { setValue(""); }
return [value, handleChange, reset];
}
TodoList.js
import React from "react";
import Paper from '#mui/material/Paper';
import List from '#mui/material/List';
import TextField from '#mui/material/TextField';
import ListItem from '#mui/material/ListItem';
import ListItemText from '#mui/material/ListItemText';
export default function ({tasksTxts}) {
return(
<div>
<Paper>
<List>
{tasksTxts}
</List>
</Paper>
</div>
)
}
Thanks in advance!
The issue is in tasksTxts, it's rendering an object instead of valid JSX.
const tasksTxts = tasksVal.map((task) =>
<>
<ListItem>
<ListItemText>
{task.txt} // <-- task.txt is object { taskTxt }
</ListItemText>
</ListItem>
<Divider/>
</>
);
...
let addToDo = (taskTxt) => {
tasksEdit([
...tasksVal,
{
id: 4,
txt: { taskTxt }, // <-- caused here
completed: false
}
])
};
The tasksTxts needs to access into the additional taskTxt property:
const tasksTxts = tasksVal.map((task) =>
<>
<ListItem>
<ListItemText>
{task.txt.taskTxt}
</ListItemText>
</ListItem>
<Divider/>
</>
);
Or the task.txt just needs to be the value you want to display:
let addToDo = (taskTxt) => {
tasksEdit(tasksVal => [
...tasksVal,
{ id: 4, txt: taskTxt, completed: false }
])
};
Unrelated Suggestion
You will want to also add a valid React key to the mapped tasks.
Example:
const tasksTxts = tasksVal.map((task) =>
<React.Fragment key={task.id}> // <-- Valid React key
<ListItem>
<ListItemText>
{task.txt}
</ListItemText>
</ListItem>
<Divider/>
</React.Fragment>
);
You are correct, the error is inside the addToDo() function:
let addToDo = (taskTxt) => {
tasksEdit([...tasksVal, { id: 4, txt: {taskTxt}, completed: false }])
// ^^^^^^^^^ the error is here
};
taskTxt is being wrapped inside another object instead of just being put on directly.
So the correct version of this code is:
tasksEdit([...tasksVal, { id: 4, txt: taskTxt, completed: false }])
// ^^^^^^^ note no wrapping braces any more

How do I make Material UI's TextField dropdown selector open with a limited size of items?

I am currently working on a Sign up Page, whenever a user enters their location, they will be prompted by a dropdown list of items of different countries, however, when the user opens that list, it covers the whole screen. How would I edit Material UI's TextField component to make it only list a certain number of items at a time without scrolling? Like 5 at a time maybe?
Here is the code in question:
import { useMemo } from 'react'
import {MenuItem, TextField, InputAdornment } from '#mui/material'
import countryList from 'react-select-country-list'
import AddLocationIcon from '#mui/icons-material/AddLocation';
import { useDispatch } from 'react-redux';
import { createUsers } from '../../reducers/usersReducer';
const CountryOfOrigin = () => {
const dispatch = useDispatch()
const options = useMemo(() => countryList().getData(), [])
const changeHandler = (e) => {
e.preventDefault();
dispatch(createUsers(e.target.value, 'location'))
}
return (
<TextField onChange={changeHandler} select className='users-location-input' required={true} label='Location' defaultValue='' InputProps={{
startAdornment: (
<InputAdornment>
<AddLocationIcon color='primary'/>
</InputAdornment>
)
}}>
{options.map(option => {
return (<MenuItem key={option.value} value={option.label}>
{option.label}
</MenuItem>)
})}
</TextField>
)
}
export default CountryOfOrigin
Furthermore,
Here are some screenshots showing the current size of the dropdown list when you open it:
You can try adding a maxHeight property to the SelectProps of the textfield component. Also, I believe you should rather use an MUI autocomplete if you want autocompletion and dropdown both.

Can't update Select value on update (MUI Select and react-hook-form)

I have been losing my mind over a MUI Select component. It was working fine until I had to use it with i18next, which changes my Select data on the rerender. That forced me to use react-hook-form with a Controller and a register.
I am still getting warnings though - every time I change my app's language I get a MUI: You have provided an out-of-range value warning.
I can't figure out what else is missing - could anyone help me with that? I'm guessing something's off with my useEffect, as when I change the app's language the default value remains as the previous one, and then I get this out-of-range value warning.
Example:
FR
"component": {
"firstOption": "Rouge"
"secondOption": "Vert"
}
EN
"component": {
"firstOption": "Red",
"secondOption": "Green"
}
What happens is that if my current language is FR, then my default value is "Rouge", but then as soon as I change the language to EN, the default value should change to "Red" but it's not doing so, it's still "Rouge", then I get this "out-of-range" value warning.
Here is my code:
Select.tsx
import { useState, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { SelectChangeEvent } from '#mui/material';
type SelectProps = {
defaultValue: string;
options: string[];
};
const Select = ({ defaultValue, options }: SelectProps) => {
const [anchorEl] = useState<null | HTMLElement>(null);
const [selectData, setSelectData] = useState('');
const open = Boolean(anchorEl);
const { control, register } = useForm();
const registerData = register('selectData');
const handleChange = (event: SelectChangeEvent) => {
setSelectData(event.target.value);
};
useEffect(() => {
setSelectData(defaultValue);
}, [defaultValue]);
return (
<Controller
name="Select"
control={control}
defaultValue={''}
render={() => (
<Select
aria-controls={open ? 'select' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
id="select"
{...registerData}
value={selectData}
onChange={(e: any) => {
registerData.onChange(e);
handleChange(e);
}}
>
{options.map((option) => (
<div key={option} value={option}>
{option}
</div>
))}
</Select>
)}
/>
);
};
export default Select;
Then when using the component:
App.tsx
import { useTranslation } from 'react-i18next';
import Select from '../../shared/Select/Select';
function App() {
const { t } = useTranslation('pages');
return (
<div>
<Select
defaultValue={t('component.firstOption')}
options={[t('component.firstOption'), t('component.secondOption')]}
/>
</div>
);
}
export default App;

MUI Autocomplete remove option after selection

I have a Material-UI Autocomplete component. In order to prevent the user from selecting the same element twice (it would double id numbers) I'd like the element removed from the drop down entirely.
For example, if "Shawshank Redemption" was selected, it should get added to the list and be removed from the drop down entirely but not change the JSON data.
I've tried using a filter on filterOptions, but that doesn't seem to be working.
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import HighlightOffIcon from "#material-ui/icons/HighlightOff";
import IconButton from "#material-ui/core/IconButton";
export default function Playground() {
const defaultProps = {
options: top100Films,
getOptionLabel: (option) => option.title,
filterOptions: (options, state) => {
let newOptions = [];
options.forEach((option) => {
if (option.title.includes(state.inputValue)) {
newOptions.push(option);
}
});
return newOptions.filter((movie) => !movies.includes(movie));
}
};
const [movies, setMovies] = useState([]);
const [key, setKey] = useState(0);
return (
<div style={{ width: 300 }}>
<Autocomplete
{...defaultProps}
id="select-on-focus"
renderInput={(params) => (
<TextField {...params} label="movies" margin="normal" />
)}
onChange={(e, movie) => {
if (movie) {
// this line prevents an error if no movie is selected
setMovies([...movies, movie.title]);
}
// this setKey is supposed to clear the Autocomplete component by forcing a rerender.
// Works in my project but not here.
setKey(key + 1);
}}
/>
<List>
{movies.map((movie) => (
<ListItem key={movie.title}>
<ListItemText primary={movie} />
<IconButton
key={key}
aria-label="delete"
onClick={() => {
setMovies(() => movies.filter((m) => m !== movie));
}}
>
<HighlightOffIcon />
</IconButton>
</ListItem>
))}
</List>
</div>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
{ title: "The Godfather: Part II", year: 1974 },
{ title: "The Dark Knight", year: 2008 },
];
See also: https://codesandbox.io/s/autocomplete-remove-from-list-5dvhg?file=/demo.js:0-6677
Edit: My project just got updated to MUI5, so I'm working on getting full functionality back, then I'll tackle this problem.
Set Autocomplete mode to multiple and turn on filterSelectedOptions to remove the selected option in the dropdown list. To display a list of selected options properly outside of the Autocomplete input, see this other answer.
const [movies, setMovies] = useState([]);
<Autocomplete
{...defaultProps}
multiple
filterSelectedOptions
renderTags={() => null} // don't render tag in the TextField
value={movies}
onChange={(e, newValue) => setMovies(newValue)}
/>
Live Demo
In order to prevent the user from selecting the same element twice (it would double id numbers) I'd like the element removed from the drop down entirely.
How about disabling them so that they cannot be selected again?
You can pass a getOptionsDisabled just like the getOptionLabel

Material UI Autocomplete implementation with options containing array of objects (ID and label)

I am using Material UI Autocomplete for my project. As shown in official documentation, my options are,
let options = [
{ id: "507f191e810c19729de860ea", label: "London" },
{ id: "u07f1u1e810c19729de560ty", label: "Singapore" },
{ id: "lo7f19re510c19729de8r090", label: "Dhaka" },
]
Then, I am using Autocomplete as,
import React, { Component, Fragment, useState } from "react"
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import options from "/options"
function SelectLocation(props){
const [ input, setInput ] = useState("");
const getInput = (event,val) => {
setInput(val);
}
return (
<Autocomplete
value={input}
options={options}
renderOption={option => <Fragment>{option.label}</Fragment>}}
getOptionLabel={option => option.label}
renderInput={params => {
return (
<TextField
{...params}
label={props.label}
variant="outlined"
fullWidth
/>
)
}}
onInputChange={getInput}
/>
)
}
Now my UI (options list) is showing what I expected. The problem is, I am getting London or Singapore as a value of my input, but I want to get the selected object or ID from this input.
I've followed their documentation thoroughly, but couldn't find a way!
onInputChange get's fired with the actual content of the input.
You might want to use the onChange event exposed by the input props, which will return the selected element. The id should then be available as val.id in your getInput callback.

Categories