I have a React component called order details. There I print 7 details about the order based on this array.
const items = [
{
name: "Order ID",
value: "01234567890",
},
{
name: "Channel ID",
value: "88888",
},
{
name: "Sales Channel",
value: "Shopify",
},
{
name: "Order Date",
value: "23-09-2022 | 00:00:00",
},
{
name: "Order Total",
value: "£10.00 GBP",
},
{
name: "Shipping Method Selected",
value: "Method",
},
{
name: "Tags",
value: "Shopify",
},
];
In my component, I loop this array and print it like this
items.map(item=><div data-testid={item.name}>
<div>
{item.name}
</div>
<div>
{item.value}
</div>
</div>)
I do want to write a unit test to this that will make sure the component is rendered successfully with the data.
I can easily write for name properties like this
const orderIdDiv = screen.getByText(/order id/i);
const chanelIdDiv = screen.getByText(/channel id/i)
expect(orderIdDiv).toBeInTheDocument();
expect(chanelIdDiv).toBeInTheDocument();
But for the value properties, I want to make sure that the value is printed in the relevant div (Eg:- value 88888 should be printed next to the Chanel ID div. Not next to something else )
So I wrote this code
// GET THE ELEMENT BY TEST ID
const orderIdDataElement = screen.getByTestId(/order id/i);
// GET THE TEXT IN THAT ELEMENT
const orderIdData = within(orderIdDataElement).getByText(/012345678901222/i);
expect(orderIdLabelElement).toBeInTheDocument();
expect(orderIdData).toBeInTheDocument();
This test always passes please note that the getByText 012345678901222 is wrong. Still, the test got passed.
What is wrong with my code?
I reproduced your example and the tests failed, like they should. It is OK to access elements by regular expression like you have done, although it may not be the best way.
When I changed the orderIdData test to
const orderIdData = within(orderIdDataElement).getByText(/01234567890/i);
expect(orderIdData).toBeInTheDocument();
the test passed.
I can't reproduce the problem. There must be some missing information that you haven't included in your question.
Also note that your test has the line
expect(orderIdLabelElement).toBeInTheDocument();
but you haven't shown us where orderIdLabelElement is defined.
My preferred method of testing your React component would be to loop through the items and, for each one, test that the textContent property of the div is as expected. Here is a working test file that does this:
import React from 'react';
import '#testing-library/jest-dom';
import { render, screen } from "#testing-library/react";
const items = [
{
name: "Order ID",
value: "01234567890",
},
{
name: "Channel ID",
value: "88888",
},
{
name: "Sales Channel",
value: "Shopify",
},
{
name: "Order Date",
value: "23-09-2022 | 00:00:00",
},
{
name: "Order Total",
value: "£10.00 GBP",
},
{
name: "Shipping Method Selected",
value: "Method",
},
{
name: "Tags",
value: "Shopify",
},
];
items.map((item) => {
it(`renders the correct content for ${item.name}`, () => {
render(
// *** Replace this with the code that renders your component ***
<>
{items.map((item) => (
<div key={item.name} data-testid={item.name}>
<div>
{item.name}
</div>
<div>
{item.value}
</div>
</div>
))}
</>
);
const renderedItem = screen.getByTestId(item.name);
expect(renderedItem.textContent).toEqual(`${item.name}${item.value}`)
});
});
A couple of extra points:
I've added a key property to the mapped items to silence a warning from React. The React docs explain why this is recommended.
It is best practice not to get elements by Test ID if you can avoid it, but I won't go into this in detail as your question title specifically states test-dataid. You can read more about this in the Testing Library docs.
Hope this helps. Happy coding!
Related
I am trying to use react-select in combination with match-sorter as described in this stackoverflow answer (their working version). I have an initial array of objects that get mapped to an array of objects with the value and label properties required by react-select, which is stored in state. That array is passed directly to react-select, and when you first click the search box everything looks good, all the options are there. The onInputChange prop is given a call to matchSorter, which in turn is given the array, the new input value, and the key the objects should be sorted on. In my project, and reproduced in the sandbox, as soon as you type anything into the input field, all the options disappear and are replaced by the no options message. If you click out of the box and back into it, the sorted options show up the way they should. See my sandbox for the issue, and here's the sandbox code:
import "./styles.css";
import { matchSorter } from "match-sorter";
import { useState } from "react";
import Select from "react-select";
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: <div>{obj.name}</div>,
name: obj.name
};
};
export default function App() {
const [options, setOptions] = useState(objs.map((obj) => myMapper(obj)));
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(matchSorter(options, val, { keys: ["name", "value"] }));
}}
/>
);
}
I am sure that the array in state is not getting removed or anything, I've console logged each step of the way and the array is definitely getting properly sorted by match-sorter. It's just that as soon as you type anything, react-select stops rendering any options until you click out and back in again. Does it have something to do with using JSX as the label value? I'm doing that in my project in order to display an image along with the options.
I had to do two things to make your code work:
Replaced label: <div>{obj.name}</div> with label: obj.name in your mapper function.
I am not sure if react-select allows html nodes as labels. Their documentation just defines it as type OptionType = { [string]: any } which is way too generic for anything.
The list supplied to matchSorter for matching must be the full list (with all options). You were supplying the filtered list of previous match (from component's state).
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: obj.name, // -------------------- (1)
name: obj.name
};
};
const allOptions = objs.map((obj) => myMapper(obj));
export default function App() {
const [options, setOptions] = useState(allOptions);
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(
matchSorter(
allOptions, // ----------------> (2)
val,
{ keys: ["name", "value"]
}
));
}}
/>
);
}
I have multiple react-select inputs, each have their own separate options array. The issue i'm having is I'm now sure how to properly store the output of each select input.
I want to use this select output to POST to my backend but i'm unsure how to do it all with a single handler function.
This is what I have so far, i have 2 paragraphs to just output the appropriate result from the select fields but i can't seem to get it working.
This is the codesandbox i have:
https://codesandbox.io/s/busy-yonath-rlj9e?file=/src/App.js
In your code you are using the useState() hook and everytime a user selects an option, you are setting your state variable to the selected value.
The problem is that everytime you run the setSelectOptions({ e }) function, you are overwriting the existing data with the value of 'e'.
What you can do is:
Create a selectHandler function that takes in 2 arguments (the new value and the value it corresponds to in the state variable)
The code will look something like this:
import "./styles.css";
import Select from "react-select";
import { useEffect, useState } from "react";
export default function App() {
const [selectOptions, setSelectOptions] = useState({
brand: "",
model: "",
color: "",
bodyType: "",
fuelType: "",
transmission: "",
location: ""
});
const brandOptions = [
{ value: "bmw", label: "Bmw" },
{ value: "audi", label: "Audi" },
{ value: "toyota", label: "Toyota" },
{ value: "nissan", label: "Nissan" }
];
const transmissionOptions = [
{ value: "automatic", label: "Automatic" },
{ value: "manual", label: "Manual" }
];
useEffect(() => console.log(selectOptions), [selectOptions])
const handleChange = (e, type) => {
setSelectOptions((previousState) => ({
...previousState,
[type]: e.value,
}));
};
return (
<div className="App">
selected brand: {selectOptions.brand}
<br />
selected transmission:{selectOptions.transmission}
<div className="mb-2" style={{ width: "40%", margin: "0 auto" }}>
<Select
value={selectOptions.brand}
onChange={(e) => handleChange(e, "brand")}
placeholder="Select brand"
options={brandOptions}
/>
<Select
value={selectOptions.transmission}
onChange={(e) => handleChange(e, "transmission")}
placeholder="Select transmission"
options={transmissionOptions}
/>
</div>
</div>
);
}
Just as an explanation, all I am doing in the setSelectOptions() function is passing in the previous values of the state variable and updating the value coinciding to the select field.
Note: Insert this code into your project, I ran it and it worked so let me know if it did help!
I've currently implemented a means of setting my Formik initial values from an API call which is all working fine but I'm currently facing an issue with re-populating my react-select field from my initial values.
At the moment, my initial values (snippet) from API fetch call looks like this:
const emptyGroup = {
groupName: "",
groupValues: []
}
const INITIAL_FORM_STATE = {
myName: '',
allGroups: [emptyGroup]
};
"allGroups": [
{
"groupName": "Group Astro",
"groupValues": [
{
"value": "My Group A",
"label": "My Group A"
},
{
"value": "My Group B",
"label": "My Group B"
}
]
}
]
Below is my react-select component:
<ReactSelect
options={ myGroupOptions }
isMulti={true}
name={`allGroups.${index}.groupValues`}
onChange={(option) => formikProps.setFieldValue({`allGroups.${index}.groupValues`}, option.value)}
onBlur={formikProps.handleBlur}
value={ ????? }
/>
What I am unsure is, how do I feed the values from the groupValues array above back into value={ ?????? } so that when this <ReactSelect /> component is rendered, it displays the values: My Group A My Group B within it?
Just in case someone else comes across this issue/query, I managed to solve the issue as follows for value together with formik values:
<ReactSelect
options={ myGroupOptions }
isMulti={true}
name={`allGroups.${index}.groupValues`}
onChange={(option) => formikProps.setFieldValue({`allGroups.${index}.groupValues`}, option.value)}
onBlur={formikProps.handleBlur}
value={values.allGroups[index].groupValues}
/>
I'm designing a form in React that has a main form builder (Create Job.js) and some form pages (AdditionalInfo.js) and (Confirmation.js). this form had a tag input that allows you to choose tags from a drop-down list provided by an API. the selected items need to be shown later in the confirmation page.
This is my main form builder that has props and functions:(CreateJob.js)
state = {
step:1,
Title:'',
requirements:'',
Location:'',
Benefits:'',
Company:'',
InternalCode:'',
Details:'',
Tags:[],
Address:'',
Department:'',
Salary:''
}
handleDropDown = input => value => {
this.setState({ [input]: value });
}
render () {
const { step } = this.state
const {Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location } = this.state;
const values ={Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location}
return (
<div>
....
<AdditionalInfo
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange={this.handleChange}
handleChangeRaw={this.handleChangeRaw}
handleDropDown={this.handleDropDown}
values={values}
/>
<Confirmation
nextStep={this.nextStep}
prevStep={this.prevStep}
values={values}
/>
....
and this is my form page which includes the list from API and the drop down using react-select(AdditionalInfo.js):
export class AdditionalInfo extends Component {
state = {
locations:[],
departments: [],
tagsList:[],
}
componentDidMount() {
axios.get('/api/jobs/list-tags',{headers:headers}).then(respo =>{
console.log(respo.data)
this.setState({
tagsList:respo.data.map(Tags=>({label: Tags.name, value: Tags.id}))
})
console.log(this.state.tagsList)
})
}
render() {
const {values, handleDropDown} = this.props
<Select placeholder='Select from pre-created Tags 'onChange={handleDropDown('Tags')} defaultValue={values.Tags} required isMulti options={this.state.tagsList}/>
...
this is the list of tags received from the API:
Object { label: "MongoDB", value: 1 }
Object { label: "JavaScript", value: 2 }
Object { label: "HTML", value: 3 }
Object { label: "CSS", value: 4 }
...
And this is my Confirmation page which needs to show the info received from previous pages (Confirmation.js)
.....
render () {
const {
values: {
Title, Benefits,
Company, InternalCode, Detailss, Department,Tags, Salary,requirements,Location
}} = this.props
<Row> Tags: {Tags.join(", ")}</Row>
....
the problem is that, instead of showing tags on the page like putting the labels next to each other
:JavaScript,
MongoDB,
... it shows this
: [object Object], [object Object], [object Object], [object Object]. sorry for the long code but Im a beginner in JavaScript and I dont know how to handle it so it shows the labels. How can I achieve this?
You are doing great, and you have done right, just simple tweak you need.
If React show anything like [Object Object] it means you are trying to render Javascript Object not a single value because you have got Tags from props which is an Array of objects.
Use it like this, it will work like butter -
import React from 'react';
const Confirmation = () => {
const tags = [ // which you got from props
{ label: "MongoDB", value: 1 },
{ label: "JavaScript", value: 2 },
{ label: "HTML", value: 3 },
{ label: "CSS", value: 4 }
];
return (
<div>
{tags.map(tag => tag.label).join(', ')} {/* map over tags to get the array of tag labels */}
</div>
);
}
export default Confirmation;
I am trying to get my head around props and how they work exactly. Here is my layout so far.
I have created a page called "TodoData.js" which has all of my Todos
const todoss = [
{
id: 1,
text: "First Todo"
},
{
id: 2,
text: "Second Todo"
},
{
id: 3,
text: "Third Todo"
},
{
id: 4,
text: "Fourth Todo"
}
]
export default todoss;
I then have my main page "Todolist.js", I have imported the data with "import TodoData from './TodoData'" at the top but I can't figure out exactly how to take that data and map it out onto the page, how would i do this?
You can use map() function to iterate an array.
import TodoData from './TodoData'
render() {
return (
<div>
{TodoData.map(function(data, idx){
return (<li key={idx}>{data.id}:{data.text}</li>)
})}
</div>
);
}
This is the output:
1:First Todo
2:Second Todo
3:Third Todo
4:Fourth Todo
You can use any styling you need.
Saving data internally as state is the "React" way of handling data.
In a real world application this data is going to come from an external source and if the developer doesn't know how to save data internally he will have no idea what to do.
components-and-props
state
Don't import the data, save it in the state of your Todos component and pass it as props to Todolist.
// this will act as a presentation of our data
const TodosList = ({ todos }) => (
<ul>
{todos.map(({ id, text }) => (
<li key={id}>{text}</li>
))}
</ul>
);
// This will act as a container for our data
class Todos extends React.Component {
state = {
todos: [
{
id: 1,
text: "First Todo"
},
{
id: 2,
text: "Second Todo"
},
{
id: 3,
text: "Third Todo"
},
{
id: 4,
text: "Fourth Todo"
}
]
};
render() {
return <TodosList todos={this.state.todos} />;
}
}