Recently started writing test cases for react component with very little knowledge. I have a component which contain a dropdown showing name of the fruits and a add button. After selecting a fruit from the list user have to click on Add button to show that fruit as a card (with name and image) in bottom section. Here 2 things are happening on Add button click -
A new card is added in the bottom section show image of the fruit and name
Selected fruit is removed from the dropdown list.
Sample code here - https://codesandbox.io/s/tender-paper-2i14r?file=/src/App.js
import { useState } from "react";
import "./styles.css";
const Fruits = ["apple", "orange", "banana"];
const FruitList = ({ onChange, fruitList = [] }) => {
return (
<select onChange={onChange} aria-label="selectFruitFromList">
<option selected>Select fruit</option>
{fruitList.map((fruit) => (
<option value={fruit}>{fruit}</option>
))}
</select>
);
};
const SelectedFruit = ({ selectedFruit }) => {
return (
<ul>
{selectedFruit.map((item) => (
<li aria-label="selectedFruits">{item}</li>
))}
</ul>
);
};
export default function App() {
const [selected, setSelected] = useState();
const [fruitList, setFruitList] = useState(Fruits);
const [selectedFruit, setSelectedFruit] = useState([]);
const addFruitToTray = () => {
setSelectedFruit([...selectedFruit, selected]);
setFruitList(fruitList.filter((item) => item != selected));
setSelected("");
};
return (
<div className="App">
<h1 aria-label="hello">Hello CodeSandbox</h1>
<div>
<FruitList
onChange={(e) => setSelected(e.target.value)}
fruitList={fruitList}
/>
<button
onClick={addFruitToTray}
disabled={selected ? false : true}
aria-label="AddFruit"
>
Add
</button>
</div>
<div>
<SelectedFruit selectedFruit={selectedFruit} />
</div>
</div>
);
}
test("Select fruit and Add", ()=> {
const { debug } = render( <App/> );
await userEvent.selectOptions(screen.getByLabelText('selectFruitFromList'), 'apple' )
expect(screen.getByLabelText('AddFruit').disabled).toBeFalsy()
await userEvent.click(screen.getByLabelText('AddFruit'))
await userEvent.selectOptions(screen.getByLabelText('selectFruitFromList'), 'banana' )
await userEvent.click(screen.getByLabelText('AddFruit'))
debug()
// Test fails here
await waitFor(() => {
expect(screen.getAllByLabelText('selectedFruits')).toBeInTheDocument();
});
})
Problem -
It fails on my last test where I'm checking li for added fruits. I have check it with debug and the HTML shows empty <ul> tag (means no <li> created). The debug error shows TestingLibraryElementError: Unable to find a label with the text of: selectedFruits
Another point I see all 3 items in the dropdown element in debug html. Ideally it shouldn't be as when we click on Add button that item gets removed from dropdown.
Any help will be appreciated. Thank you.
I have generated new test project with npm init react-app testing and i have tried your code. It gives this error:
expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Received has type: array
Received has value: [<li aria-label="selectedFruits">apple</li>, <li aria-label="selectedFruits">banana</li>]
In order to make it work use .toHaveLength(2) assertation. I have tried the same in codesandbox, but for some reason I could not make it work.
For more assert methods, this is good place to look: https://github.com/testing-library/jest-dom
Related
I have made autocomplete features using Downshift using react js. But the problem is when I am searching for something its input field value is disappearing when I click on the outside. Here is the sample code.
import logo from './logo.svg';
import './App.css';
import React, { useState } from "react";
import Highlighter from "react-highlight-words";
import Downshift from "downshift";
import axios from 'axios';
function App() {
const [names, setnames] = useState([{
const [searchTerm, setSearchTerm] = useState('')
const [movie, setmovie] = useState([])
fetchMovies = fetchMovies.bind(this);
inputOnChange = inputOnChange.bind(this);
function inputOnChange(event) {
if (!event.target.value) {
return;
}
fetchMovies(event.target.value);
}
function downshiftOnChange(selectedMovie) {
alert(`your favourite movie is ${selectedMovie.title}`);
}
function fetchMovies(movie) {
const moviesURL = `https://api.themoviedb.org/3/search/movie?api_key=1b5adf76a72a13bad99b8fc0c68cb085&query=${movie}`;
axios.get(moviesURL).then(response => {
setmovie(response.data.results);
// this.setState({ movies: response.data.results });
});
}
return (
<Downshift
onChange={downshiftOnChange}
itemToString={item => (item ? item.title : "")}
>
{({
selectedItem,
getInputProps,
getItemProps,
highlightedIndex,
isOpen,
inputValue,
getLabelProps
}) => (
<div>
<label
style={{ marginTop: "1rem", display: "block" }}
{...getLabelProps()}
>
Choose your favourite movie
</label>{" "}
<br />
<input
{...getInputProps({
placeholder: "Search movies",
onChange: inputOnChange
})}
/>
{isOpen ? (
<div className="downshift-dropdown">
{movie
.filter(
item =>
!inputValue ||
item.title
.toLowerCase()
.includes(inputValue.toLowerCase())
)
.slice(0, 10)
.map((item, index) => (
<div
className="dropdown-item"
{...getItemProps({ key: index, index, item })}
style={{
backgroundColor:
highlightedIndex === index ? "lightgray" : "white",
fontWeight: selectedItem === item ? "bold" : "normal"
}}
>
{item.title}
</div>
))}
</div>
) : null}
</div>
)}
</Downshift>
);
}
export default App;
This is the sample code I have written. Also, when I click shift+home, it is also not working.
Problem 1: when the user clicked the outside text field value whatever I searched this is disappearing.
Problem 2: shift + home is not working also.
Anyone has any idea how to solve this problem?
when the user clicked the outside text field value whatever I searched this is disappearing.
One way you could do it is to set the stateReducer on the Downshift component:
This function will be called each time downshift sets its internal state (or calls your onStateChange handler for control props). It allows you to modify the state change that will take place which can give you fine grain control over how the component interacts with user updates without having to use Control Props. It gives you the current state and the state that will be set, and you return the state that you want to set.
state: The full current state of downshift.
changes: These are the properties that are about to change. This also has a type property which you can learn more about in the stateChangeTypes section.
function stateReducer(state, changes) {
switch (changes.type) {
case Downshift.stateChangeTypes.mouseUp:
return {
...changes,
isOpen: true,
inputValue: state.inputValue,
};
default:
return changes;
}
}
This way if you click outside the text field the dropdown will stay open and the input value won't be reset.
For a list of all state change types see the documentation here
You might also be able to get something working using the onBlur prop on the input, but I didn't get that working.
Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)
I've just started learning React. I'm working on a project for practice. It has two different components Nav & Main.I'm calling these components in App.js. My Nav component has a number of coloured divs which show up on clicking the plus icon. Now, I want to create a new div in Main.js on click of a coloured div in Nav.js. This new div should have the same background colour as the one we click on. I'm confused how to do it. Please! Help me out.
This is my Nav component.
const Nav = () => {
const [showColors, setShowColors] = useState(false);
return(
<div className="NavBar">
<h1 className="NavHeading">Notes.</h1>
<div onClick={() => setShowColors(!showColors)} className="PlusImg"></div>
<div className={showColors ? "" : "ColorBar"}>
<div className="Color Color1"></div>
<div className="Color Color2"></div>
<div className="Color Color3"></div>
<div className="Color Color4"></div>
<div className="Color Color5"></div>
</div>
</div>
);
};
export default Nav;
This is my Main component.
const Main = () => {
return(
<div className="Main">
<div className="NoteItem"></div>
</div>
);
};
export default Main;
I want to create new div with class name of NoteItem as the one written in the Main component.
I'm not 100% sure I understand what you're trying to do. Apologies if I've misunderstood.
I'd approach this by keeping track of the items that have been added in component state:
const App = () => {
// starting with an empty array
const [items, setItems] = React.useState([]);
And defining a handler for adding an item:
const App = () => {
const [items, setItems] = React.useState([]);
// itemClass is just a string, e.g. "color1" or "color5"
const addItem = itemClass => setItems([...items, itemClass]);
The spread syntax used above creates a new array containing the previous array plus the new item:
const oldArr = ['color1','color2'];
const newArr = [...oldArr, 'color3']
// newArr is now ['color1', 'color2', 'color3'];
You can then pass the items to Main as a prop, which can render a div for each item:
const App = () => {
const [items, setItems] = React.useState([]);
const addItem = itemClass => setItems([...items, itemClass]);
return (
<Main items={items} /> {/* pass items array to Main */}
);
}
const Main = ({items}) => { // {items} is the equivalent of props.items
// render a div for each item in the items array
return items.map( item => (
<div className={item}>This item has a class of {item}</div>
)
}
And pass the addItem handler to Nav so it can tell App to add the clicked item:
const Nav = ({addItem}) => {
return (
<div className={showColors ? "" : "ColorBar"}>
<div onClick={() => addItem('Color1')} className="Color Color1"></div>
{/* repeat for Color2, etc. */}
</div>
);
}
With this place, when a Nav div is clicked an item will get added to App's items array, which will trigger a re-render, passing the updated array to Main, and you'll see the new div.
a bit of housekeeping:
In the Main component above you're going to get a react key warning. I omitted the key in the interest of readability, but you'll need to include a key prop that's unique for each item. The easiest way to do this is to just use the index of the iteration:
const Main = ({items}) => {
// add key={index} to make react happy
return items.map((item, index) => (
<div key={index} className={item}>This item has a class of {item}</div>
)
}
Also, the items.map call will blow up if items isn't provided. You can dodge this by setting it to an empty array by default:
const Main = ({items = []}) => {
// ...
}
First of all you need to define your state in your App component and pass it to your components as an argument because both your Nav and Main components needs to access your state. You can do it like this
class App extends React.Component{
constructor(){
super()
this.state = {
showColors: false
}
}
}
After that you can take elements color with the onClick event listener. Define it in your state as well. And create a div element inside your Main component with it.
Dont forget to pass it as an argument to your Main component too.
In the following component, if I click on any of the two buttons, the URL in the address-bar gets changed.
In the list view if you click on the button Details the page get rendered and shows that particular item and the URL in the address bar get changed too.
And in the user view if you click on the "Back to overview" button, the page renders back to the list view and the URL gets changed again.
import React, { useState, useEffect, Fragment } from 'react'
import axios from 'axios'
const UserList = ({ id, setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me'
)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
const renderItem = (item, userId) => {
const setURL = (e) => {
window.history.pushState(null, null, '/' + e)
setID(item.login.uuid)
}
const clearURL = (e) => {
window.history.back()
setID(null)
}
return (
<Fragment key={item.login.uuid}>
{userId ? (
// User view
<div>
<img src={item.picture.large} />
<h2>
{item.name.first}
</h2>
<p>
{item.phone}
<br />
{item.email}
</p>
<button onClick={() => clearURL('/')}>
Back to overview
</button>
</div>
) : (
// List view
<li>
<img src={item.picture.large} />
<h2>
{item.name.first}
</h2>
<button onClick={() => setURL(item.login.uuid)}>
Details
</button>
</li>
)}
</Fragment>
)
}
const user = resources.find(user => user.login.uuid === id)
if (user) {
// User view
return <div>{renderItem(user, true)}</div>
} else {
// List view
return (
<>
<ul>
{resources.map(user => renderItem(user, false))}
</ul>
</>
)
}
}
export default UserList
Everything is working fine.
However, the problem with this solution is that on user view, I cannot use the browsers back button to go back to the list view page.
Is there any way I can change this without using React Route?
So what I believe is happening is you are over-writing the 'history' of the browser. The 'history' is pretty much just a stack of paths you've been and when you click the back button it just pops the current path off the stack. I think that when your using 'window.history.pushState(null, null, '/' + e)' its setting the history = to null so there is nothing to go back to. I would recommend using react-router and just pushing a path ie. router.push('/whatever'). There are a bunch of ways to interact with the history through react without using the native history object. If you want to use the native history object https://developer.mozilla.org/en-US/docs/Web/API/History_API
Edit - I apologize your not overwritting the history, but I do still believe that the error is coming from passing a 'null' value as the 'state' param for the pushState method
I am creating a todo app in React that is basically just a list of items, grouped by category. I want to add functionality such that when I click a single to do(which is a paragraph), it brings up an input with the current text that I can edit and save. How can I do that without manually editing the DOM?
Code:
A single todo item:
import React from 'react';
const Item = props => {
return (
<div
className={`item${props.item.purchased ? ' purchased' : ''}`}
onClick={() => props.toggleItem(props.item.id)}
>
<p>{props.item.name}</p>
</div>
);
};
export default Item;
I want to change the toggle to be a radio button and onClick to edit the todo.
sample image of todos
First of all, you will need a prop that updates item.name (this will be needed when you will edit the input)
You didn't explained well how you want it to work, so I made an example where you click on the text to edit it to a text input and also have a button to save the edit.
const Item = props => {
const [isEditing, setIsEditing] = useState(false);
return isEditing ? (
<div>
<input
value={props.item.name}
onChange={e => props.setItemName(e.target.value)}
/>
<button onClick={() => setIsEditing(false)}>Stop Editing</button>
</div>
) : (
<div
className={`item${props.item.purchased ? " purchased" : ""}`}
onClick={() => setIsEditing(true)}
>
<p>{props.item.name}</p>
</div>
);
};
I also created a working codesanbox with the behavior you want.
You will have to maintain a state to change between the TODO Item & TODO Input. Since you are using functional component, you can use useState hook from react to maintain the state as shown below
import React, { useState } from 'react';
const Item = props => {
const [isEditing, setIsEditing] = useState(false);
if (isEditing) {
return (
<div className={`item${props.item.purchased ? ' purchased' : ''}`}>
<input type="text/radio" value={props.item.name}>
</div>
);
} else {
return (
<div
className={`item${props.item.purchased ? ' purchased' : ''}`}
onClick={() => props.toggleItem(props.item.id)}
>
<p>{props.item.name}</p>
</div>
);
}
};
export default Item;
You might need to change the above a bit based on your application structure but this is what you need to follow.