How to preset prop (logged in user) for React component unit test? - reactjs

I'm rather new to testing React application, thank you for your time in advance for responding to a newbie question.
So I've been following tutorial on Full Stack Open and came across this challenge about writing tests for React. There is this component Blog which takes some props from App > Blog List > Blog, including one called 'user' which is the returned object from the login function storing username and token etc.
In the Blog's JSX there is a 'remove' button which is shown only to logged in users, controlled by its style determined by a function comparing the username of the original poster of the blog and that of the currently logged in user.
Right now I'm not writing test for username comparison function at all, but it just gets in the way because I can't seem to set a value for 'user' to be passed into the Blog component, and this error was returned during the test:
display: blog.user.username === user.username ? '' : 'none'
^
TypeError: Cannot read properties of undefined (reading 'username')
And here are the codes of the Blog component and the test at current state:
import { useState } from 'react'
const Blog = ({ blog, addLike, deleteBlog, user }) => {
const [showDetails, setShowDetails] = useState(false)
const showWhenDetailsTrue = { display: showDetails ? '' : 'none' }
const toggleDetails = () => {
setShowDetails(!showDetails)
}
const postedBySelf = async () => {
const style = await {
display: blog.user.username === user.username ? '' : 'none',
}
return style
}
return (
<div style={blogStyle}>
<div>
{blog.title} {blog.author}{' '}
<button onClick={toggleDetails}>{showDetails ? 'hide' : 'view'}</button>
</div>
<div style={showWhenDetailsTrue} className="defaultHidden">
<div>{blog.url}</div>
<div>
likes {blog.likes}
<button onClick={() => addLike(blog.id)}>like</button>
</div>
<div>{blog.author}</div>
<button onClick={() => deleteBlog(blog)} style={postedBySelf()}>
remove
</button>
</div>
</div>
)
}
export default Blog
The test file:
import React from 'react'
import '#testing-library/jest-dom/extend-expect'
import { render, screen } from '#testing-library/react'
import Blog from './Blog'
test('renders title and author, but not url or number of likes by default', async () => {
const blog = {
title: 'Blog title',
author: 'Blog author',
url: 'Blog url',
user: {
username: 'mockuser',
},
}
await render(<Blog blog={blog} user={{ username: 'mockuser' }} />)
screen.getByText('Blog title', { exact: false })
screen.getAllByText('Blog author', { exact: false })
const { container } = render(<Blog blog={blog} />)
const div = container.querySelector('.defaultHidden')
expect(div).toHaveStyle('display: none')
})
When the postedBySelf function and associated content are commented out the test is passed. My question is, how can I mock the 'user' object and pass it into the component during the test? I don't understand why it is undefined even if I explicitly declared its value.
Thanks again for your time and appreciate your advice.

Finally spotted my mistake, had to pass in the user in the second rendering of the Blog too.
I wasn't quite sure if I'm missing critical knowledge on this topic but this tutorial explains things very well and helped me spotted the issue in a way. Strongly recommended: https://www.youtube.com/watch?v=OVNjsIto9xM

Related

How to pass props using react router link v6?

all I am trying to do where to put generateQuestion function and if I put it in App.js
how to pass props form App.js to Quiz.js
please if you have a better implementation of routing & generateQuestion function feel free to leave your recommendations
The generateQuestion callback is asynchronous, so you won't be able to send data in route state via the Link, but what you can do is move the onClick handler to the Link and call event.preventDefault on the click event to prevent the navigation action, and issue an imperative navigation after the questions have been fetched.
Example:
import { Link, useNavigate } = 'react-router-dom';
...
const navigate = useNavigate();
...
generateQuestions = async (event) => {
event.preventDefault();
try {
const data = await questionsData();
const arrData = data.map(element => ({
...element,
id: nanoid(),
}));
navigate("/quiz", { state: { questions: arrData } });
} catch(error) {
// handle rejected Promise/error/etc...
}
}
...
<Link to="/quiz" onClick={generateQuestions}>
<button type="button">
Start Quiz
</button>
</Link>
...
In the Quiz component use the useLocation hook to access the passed route state.
import { useLocation } from 'react-router-dom';
...
const { state } = useLocation();
const { questions } = state || {};
For future questions I recommend that you actually write the code instead of pasting a picture so that it's easier to try and answer you.
That aside, I had a similar situation, what I did was the following
(Keep in mind that item.document is the property of the data that you want to pass through)
Where you are getting the data in my case I created a new class:
const GetData: React.FunctionComponent<{}> = () => {
const [information, setInformation] = useState([]);
useEffect(() => {
.
.
//all the request stuff for getting the data
.
.
}
return (
<div>
{information.map((item) => (
<div key={item.id}>
<Link to="/yourUrl" state={{from:'test',
body:`${item.document}`}}>
<div>
<h3 >{item.name}</h3>
<p>{item.description}</p>
</div>
</Link>
</div>
))}
</div>
);
}
Remember that .map is in case you want to dynamically creat components depending on the data that you're getting.
Where you want to see and use the data passed:
const Impementation: React.FunctionComponent = () => {
const location = useLocation()
const [itemData, setItemData] = React.useState({
body: '',
})
.
.
.
useEffect(() => {
.
.
.
if (location.state) {
let _state = location.state as any
setItemData(_state)
}, [itemData]);
.
.
.
};
What you did was set the state with the data that came through and then you can use it as you would use states.
This video helped me a lot
https://www.youtube.com/watch?v=HLwR7fTB_NM&t=689s

React: Cannot read properties of undefined (reading 'id')

I have created react gallery which allows user to upload an image into firebase storage, then firestore reads its urls and displays in my app.
I basicaly loop through an array of images (named docs) created and exported in other file. Now I want to add possibility to delete the file so before I remove it from firebase storage I want to remove the document from firestore database.
Now, I want function deleteDocument to remove document by method deleteDoc provided by firebase.
When I fire it I get an error: Cannot read properties of undefined (reading 'id').
Seems like id is undefined but when I look into Dev Tools I clearly see there is id field (example: id: "REqWGEahJF2iqsdROM"). I have tried to get into component I want to delete by ref.current and ref.doc.id. I've been working on it for last few days and really I got no idea how to fix it :/
When I replace compRef.doc.id with compRef.current I got this error: Uncaught TypeError: n.indexOf is not a function.
import React from "react";
import { projectFirestore } from "../../firebase/config";
import { deleteDoc, doc } from 'firebase/firestore';
import useFirestore from "../../hooks/useFirestore";
const ImageGrid = ({ setSelectedImg }) => {
const compRef = React.useRef();
const { docs } = useFirestore('images');
const deleteDocument = () => {
console.log(compRef);
const docRef = doc(projectFirestore, 'images', compRef.current)
deleteDoc(docRef).then(() => {
console.log(`removed doc ${compRef.current}`)
})
}
return (
<div className="img-grid">
{ docs && docs.map(doc => (
<div className="img-wrap" key={doc.id}
ref={node => compRef.current = node}
onClick={() => setSelectedImg(doc.url)}>
<img src={doc.url} alt="uploaded img" />
<button className="delete"
onClick={deleteDocument}
>Delete</button>
<div>
))}
</div>)
}
Does anyone have an idea how to solve this?
EDIT:
answer to console.log object compRef
current: div.img-wrap
__reactFiber$ars7zzthpl: FiberNode {tag: 5, key: null, elementType:
'div', type: 'div', stateNode: div.img-wrap, …}
__reactProps$ars7zzthpl:
children: (2) [{…}, {…}]
className: "img-wrap"
onClick: () => setSelectedImg(doc.url)
...
id: ""
I think you dont need more properties of that object.
Now I see id field is empty, so is key. Still cannot solve this :(
When I console log docs array (compRef is reference to docs) then I see:
id: "REqWGEahJFQh2iqsdROM"

Why is my data that is coming from apollo server not showing up when I refresh the page?

I am building a simple application using React, Apollo and React Router. This application allows you to create recipes, as well as edit and delete them (your standard CRUD website).
I thought about how I would present my problem, and I figured the best way was visually.
Here is the home page (localhost:3000):
When you click on the title of a recipe, this is what you see (localhost:3000/recipe/15):
If you click the 'create recipe' button on the home page, this is what you see (localhost:3000/create-recipe):
If you click on the delete button on a recipe on the home page, this is what you see (localhost:3000):
If you click on the edit button on a recipe on the home page, this is what you see (localhost:3000/recipe/15/update):
This update form is where the problem begins. As you can see, the form has been filled with the old values of the recipe. Everything is going to plan. But, when I refresh the page, this is what you see:
It's all blank. I am 67% sure this is something to do with the way React renders components or the way I am querying my apollo server. I don't fully understand the process React goes through to render a component.
Here is the code for the UpdateRecipe page (what you've probably been waiting for):
import React, { useState } from "react";
import { Button } from "#chakra-ui/react";
import {
useUpdateRecipeMutation,
useRecipeQuery,
useIngredientsQuery,
useStepsQuery,
} from "../../types/graphql";
import { useNavigate, useParams } from "react-router-dom";
import { SimpleFormControl } from "../../shared/SimpleFormControl";
import { MultiFormControl } from "../../shared/MultiFormControl";
interface UpdateRecipeProps {}
export const UpdateRecipe: React.FC<UpdateRecipeProps> = ({}) => {
let { id: recipeId } = useParams() as { id: string };
const intRecipeId = parseInt(recipeId);
const { data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const { data: ingredientsData } = useIngredientsQuery({
variables: { recipeId: intRecipeId },
});
const { data: stepsData } = useStepsQuery({
variables: { recipeId: intRecipeId },
});
const originalTitle = recipeData?.recipe.recipe?.title || "";
const originalDescription = recipeData?.recipe.recipe?.description || "";
const originalIngredients =
ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text) || [];
const originalSteps = stepsData?.steps?.steps?.map((stp) => stp.text) || [];
const [updateRecipe] = useUpdateRecipeMutation();
const navigate = useNavigate();
const [formValues, setFormValues] = useState({
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<SimpleFormControl
label="Title"
name="title"
type="text"
placeholder="Triple Chocolate Cake"
value={formValues.title}
onChange={(e) => {
setFormValues({ ...formValues, title: e.target.value });
}}
/>
<SimpleFormControl
label="Description"
name="description"
type="text"
placeholder="A delicious combination of cake and chocolate that's bound to mesmerize your tastebuds!"
value={formValues.description}
onChange={(e) => {
setFormValues({ ...formValues, description: e.target.value });
}}
/>
<MultiFormControl
label="Ingredients"
name="ingredients"
type="text"
placeholder="Eggs"
values={formValues.ingredients}
onAdd={(newValue) => {
setFormValues({
...formValues,
ingredients: [...formValues.ingredients, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
ingredients: formValues.ingredients.filter(
(__, idx) => idx !== index
),
});
}}
/>
<MultiFormControl
ordered
label="Steps"
name="steps"
type="text"
placeholder="Pour batter into cake tray"
color="orange.100"
values={formValues.steps}
onAdd={(newValue) => {
setFormValues({
...formValues,
steps: [...formValues.steps, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
steps: formValues.steps.filter((__, idx) => idx !== index),
});
}}
/>
<Button type="submit">Update Recipe</Button>
</form>
);
};
I'll try to explain it as best as I can.
First I get the id parameter from the url. With this id, I grab the corresponding recipe, its ingredients and its steps.
Next I put the title of the recipe, the description of the recipe, the ingredients of the recipe and the steps into four variables: originalTitle, originalDescription, originalIngredients and originalSteps, respectively.
Next I set up some state with useState(), called formValues. It looks like this:
{
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
}
Finally, I return a form which contains 4 component:
The first component is a SimpleFormControl and it is for the title. Notice how I set the value prop of this component to formValues.title.
The second component is also a SimpleFormControl and it is for the description, which has a value prop set to formValues.description.
The third component is a MultiFormControl and it's for the ingredients. This component has its value props set to formValues.ingredients.
The fourth component is also aMultiFormControl and it's for the steps. This component has its value props set to formValues.steps.
Let me know if you need to see the code for these two components.
Note:
When I come to the UpdateRecipe page via the home page, it works perfectly. As soon as I refresh the UpdateRecipe page, the originalTitle, originalDescripion, originalIngredients and originalSteps are either empty strings or empty arrays. This is due to the || operator attached to each variable.
Thanks in advance for any feedback and help.
Let me know if you need anything.
The problem is that you are using one hook useRecipeQuery that will return data at some point in the future and you have a second hook useState for your form that relies on this data. This means that when React will render this component the useRecipeQuery will return no data (since it's still fetching) so the useState hook used for your form is initialized with empty data. Once useRecipeQuery is done fetching it will reevaluate this code, but that doesn't have any effect on the useState hook for your form, since it's already initialized and has internally cached its state. The reason why it's working for you in one scenario, but not in the other, is that in one scenario your useRecipeQuery immediately returns the data available from cache, whereas in the other it needs to do the actual fetch to get it.
What is the solution?
Assume you don't have the data available for your form to properly render when you first load this component. So initialize your form with some acceptable empty state.
Use useEffect to wire your hooks, so that when useRecipeQuery finishes loading its data, it'll update your form state accordingly.
const { loading, data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const [formValues, setFormValues] = useState({
title: "",
description: "",
ingredients: [],
steps: [],
});
useEffect(() => {
if (!loading && recipeData ) {
setFormValues({
title: recipeData?.recipe.recipe?.title,
description: recipeData?.recipe.recipe?.description,
ingredients: ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text),
steps: stepsData?.steps?.steps?.map((stp) => stp.text),
});
}
}, [loading, recipeData ]);

movie-trailer npm isn't working properly in my netflix clone project

I've made a netflix clone using React.js, firebase, and have used TMDB api for movie database.
I've also used react-youtube and movie-trailer npm.
So it has this feature that every time I click on any movie poster, it's trailer must be played.
But for most of the movies, trailer doesn't shows up.
This is the error that I face -
movie-trailer: No TMDB Movie found with the current search terms, try searching https://www.themoviedb.org/search?query=Luis%20Miguel%3A%20The%20Series
TypeError: Failed to construct 'URL': Invalid URL
at Row.js:37
'This is the screenshot of errors I am facing on clicking maximum of movies'
I'm sharing the link of my github repo and deployed website as well for reference -
github - https://github.com/IshitaSharma3101/netflix-clone
website - https://netflix-clone-afb8b.web.app/
Row component code-
import React, { useState, useEffect } from "react";
import YouTube from "react-youtube";
import axios from "./axios";
import "./Row.css";
import movieTrailer from "movie-trailer"
const base_url = "https://image.tmdb.org/t/p/original/";
function Row({ title, fetchURL, isLargeRow }) {
const [movies, setMovies] = useState([]);
const [trailerURL, setTrailerURL] = useState("");
useEffect(() => {
async function fetchData() {
const request = await axios.get(fetchURL);
console.log(request.data.results);
setMovies(request.data.results);
return request;
}
fetchData();
}, [fetchURL]);
const opts = {
height: "390",
width: "100%",
playerVars: {
autoplay: 1,
},
};
const handleClick = (movie) => {
if (trailerURL) {
setTrailerURL("");
} else {
movieTrailer(movie?.name || movie?.title || movie?.original_title || "")
.then((url) => {
const urlParams = new URLSearchParams(new URL(url).search);
setTrailerURL(urlParams.get("v"));
})
.catch((error) => console.log(error));
}
};
return (
<div className='row'>
<h2>{title}</h2>
<div className='row__posters'>
{movies.map((movie) => (
<img
key={movie.id}
onClick={() => handleClick(movie)}
className={`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`${base_url}${
isLargeRow ? movie.poster_path : movie.backdrop_path
}`}
alt={movie.name}
/>
))}
</div>
{trailerURL && <YouTube videoId={trailerURL} opts={opts} />}
</div>
);
}
export default Row;
I'm sure you are following the Clever Qazi tutorial as I'm doing. I think there is basically no solution for this problem. I also try to get the trailer of the movies by the ID that TMDB gives for every film but this method definitely doesn't work for mine. My conclusion is that movie-trailer is not working for certain kind of film or series...
You can prove yourself my conlclusion by typing on a terminal npx movie-trailer Cobra Kai
which is the command that allow you to use this package. Cobra Kai is a well know series but this "tool" doesn't find any trailer for it.
Use this instead.
movieTrailer(null ,{ tmdbId: movie.id })
.then((url)=>{
console.log("url is "+url);
const urlParams=new URLSearchParams(new URL(url).search);
console.log("urlParamsn"+urlParams);
setTrailerUrl(urlParams.get("v"));
})
.catch((error)=> console.log(error));
}
}
With this you can search for the movie with the tmdb id.
This error is coming becouse TMDB has not added ids for some videos if you want to play all video you just need need to change the genres of the Netflix Original Video from the path in request.js folder where you wrote your path becouse Netflix originals Videos has not linked with ids on TMDB.

autosuggest not showing item immediately

I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.

Resources