How can I await api results before doing a matchSnapshot test? - javascript

I have a test that does a snapshot match on the DOM. I am not sure if I am using the correct mount/render or correct testing library for this. The issue I am having is when the component is called, it awaits the API result. However, my test is not waiting for the api result even though I have put an act around it. Wondering if I have done something wrong or used incorrect function from the library.
This is what I tried.
await act(async () => {
const { container } = render(<Home />);
await mockGetMovies;
expect(container).toMatchInlineSnapshot(`
<div>
<main>
<h1>
Movies (1)
</h1>
</main>
</div>
`);
});
Snapshot is (actual)
<p>
Fetching data...
</p>
when it should be (expecting)
<main>
<h1>
Movies (1)
</h1>
</main>
Here is what the full test code looks like:
import React from 'react';
import { render } from '#testing-library/react';
import { getMovies } from '../app/requests/getMovies';
import Home from '.';
import { act } from 'react-dom/test-utils';
jest.mock('../app/requests/getMovies');
const mockGetMovies = getMovies as jest.Mock;
describe('/', () => {
beforeEach(() => {
mockGetMovies.mockResolvedValue([
{
Title: 'Titanic',
},
]);
});
it('should render the homepage', async () => {
await act(async () => {
const { container } = render(<Home />);
await mockGetMovies;
expect(container).toMatchInlineSnapshot(`
<div>
<main>
<h1>
Movies (1)
</h1>
</main>
</div>
`);
});
});
});
Here is what the Component Looks like
import React, { useEffect, useState } from 'react';
import { getMovies } from '../app/requests/getMovies';
const Home = () => {
const [movies, setMovies] = useState([]);
useEffect(() => {
getMovies().then((data) => {
setMovies(data);
});
}, []);
if (movies.length == 0) {
return <p>Fetching data...</p>;
}
return (
<main>
<h1>Movies ({movies.length})</h1>
</main>
);
};
export default Home;

Related

How to test the updated state in React after API call

I have a component like the one below for example, Since the state changes after the API is called, I am not able to test the HTML after API call finishes, it always goes to the default state
import React, { useEffect, useState } from 'react';
const App = props => {
const [user, setUser] = useState([]);
const [userLoaded, setUserLoaded] = useState(false);
const fetchUser = async () => {
try {
let response = await fetch('https://randomuser.me/api');
let json = await response.json();
return { success: true, data: json };
} catch (error) {
console.log(error);
return { success: false };
}
}
useEffect(() => {
(async () => {
setUserLoaded(false);
let res = await fetchUser();
if (res.success) {
setUser(res.data.results[0]);
setUserLoaded(true);
}
})();
}, []);
return (
<div>
{userLoaded ? (
<div>
<ul>
<li><strong>First name:</strong> {user.name.first}</li>
<li><strong>Last name:</strong> {user.name.last}</li>
<li><strong>Email:</strong> {user.email}</li>
</ul>
</div>
) : (
<p> Loading Please wait </p>
)}
</div>
);
}
export default UserID;
So for me, it always goes to the else block and all the tests fail, I want to test the userLoaded part, assuming I am going to mock the API call, how does testing update the state and get to the uerLoaded part?
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const wrapper = mount(<App />);
expect(wrapper.html()).toBe('<div>First name:</strong> {user.name.first}</div>');
});
});
I tried writing the below unit tests, but they are not working
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const wrapper = mount(<App />);
expect(wrapper.html()).toBe('First name:</strong> {user.name.first}');
});
});

how do i delay rendering of a page until my data is available?

I have an issue where my page is trying to render before the data is available. I have async awaits in place, however, the page gets an error saying data is undefined. When I comment out my page elements and check react dev tools I can see the data object in full, so I know the data request is working.
I need to put in a check for the data and if present then render but as a new developer I am not sure how to implement this in my code.
import React, { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { getDoc, doc } from "firebase/firestore";
import { db } from "../api/auth/firebase/config";
import Head from "next/head";
import ArtistHeader from "../../components/ArtistHeader";
import UploadButton from "../../components/UploadButton";
import styles from "../../styles/artistPage.module.css";
export default function Artist() {
const { data: session, status, loading } = useSession();
const [artist, setArtist] = useState();
const router = useRouter();
const artistId = router.query.artistId;
const fetchArtist = async () => {
const artistRef = doc(db, "users", `${artistId}`);
const docSnap = await getDoc(artistRef);
setArtist(docSnap.data());
};
useEffect(() => {
if (!router.isReady) return;
console.log(artistId);
if (status === "unauthenticated") {
router.push("/auth/signin");
}
fetchArtist();
}, [status, loading, router]);
return (
<section className={styles.wrapper}>
<Head>
<title>{artist.screenName}</title>
</Head>
<div className={styles.artistPage}>
<ArtistHeader artist={artist} />
<div className={styles.songContainer}>
<UploadButton />
</div>
</div>
</section>
);
}
Thanks in advance for help.
use optional chaining. This will prevent you from getting undefined error.
see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
On the other hand you can do the following:
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const fetchArtist = async () => {
setIsLoading(true)
const artistRef = doc(db, "users", `${artistId}`);
try{
const docSnap = await getDoc(artistRef);
setArtist(docSnap.data());
}catch(e){
setIsError(true)
}
setIsLoading(false)
};
if(isLoading && !artist){
return (
<h2>Loading...</h2>
)
}
if(!isLoading && isError){
return (
<h2>Something went wrong</h2>
)
}
return (
<section className={styles.wrapper}>
<Head>
<title>{artist?.screenName}</title>
</Head>
<div className={styles.artistPage}>
<ArtistHeader artist={artist} />
<div className={styles.songContainer}>
<UploadButton />
</div>
</div>
</section>
)
But I would prefer react-query for server state management. It handles all your loading | revalidation | caching and more.
Check out https://tanstack.com/query/v4/docs/adapters/react-query
Let's make it simple with useQuery hook from react-query
import { useQuery } from '#tanstack/react-query'
const fetchArtist = async (artistId: string) => {
const artistRef = doc(db, "users", `${artistId}`);
return getDoc(artistRef);
};
function Artist() {
const query = useQuery(['artist', artistId], fetchArtist)
const {isLoading, isError, data} = query
if(isLoading){
return (
<h2>Loading...</h2>
)
}
if(isError && !data){
return (
<h2>Something went wrong</h2>
)
}
return (
<section className={styles.wrapper}>
<Head>
{/* optional chaining (?.) */}
<title>{data?.artist?.screenName}</title>
</Head>
<div className={styles.artistPage}>
<ArtistHeader artist={data?.artist} />
<div className={styles.songContainer}>
<UploadButton />
</div>
</div>
</section>
)
}
// _app.jsx
import { Hydrate, QueryClient, QueryClientProvider } from '#tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
You can use getServerSideProps to call API on the server. Whenever data is ready, the page will start loading on the client-side.
import React, { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { getDoc, doc } from "firebase/firestore";
import { db } from "../api/auth/firebase/config";
import Head from "next/head";
import ArtistHeader from "../../components/ArtistHeader";
import UploadButton from "../../components/UploadButton";
import styles from "../../styles/artistPage.module.css";
export default function Artist({ data }) {
const { data: session, status, loading } = useSession();
const artist = data; //get data from the server
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/signin");
}
}, [status, loading, router]);
return (
<section className={styles.wrapper}>
<Head>
<title>{artist.screenName}</title>
</Head>
<div className={styles.artistPage}>
<ArtistHeader artist={artist} />
<div className={styles.songContainer}>
<UploadButton />
</div>
</div>
</section>
);
}
export async function getServerSideProps(context) {
const artistId = context.params.artistId;
const artistRef = doc(db, "users", `${artistId}`);
const docSnap = await getDoc(artistRef);
const data = docSnap.data();
return { props: { data: data || null } }
}
You can use a state isLoading. The initial value of isLoading will be false. Inside useEffect before fetching data set isLoading value as true and after completing fetching set isLoading as false. Now use conditional rendering if isLoading then render a Loader component else render jsx with data.

Can't access prop in another component?

I'm getting myself confused with React here (total newbie). I have a simple component that fetches some data that always returns {"score":100}:
import React, { useEffect, useState } from "react";
import Graph from "./Graph.js";
const UsingFetch = () => {
const [results, setResults] = useState({"score": null}); // initially set score to null
const fetchData = () => {
fetch("https://myapi.com/id=1")
.then((response) => {
return response.json();
})
.then((data) => {
setResults(data); // update results with integer score
});
};
useEffect(() => {
fetchData();
}, []);
console.log(results)
return (
<div>
<Graph results={results.score}></Graph>
</div>
);
};
export default UsingFetch;
My Graph.js looks like the following:
import { React } from 'react'
export default function Graph({results}) {
console.log(results)
return (
<div>
<h1>{results}</h1>
</div>
)
}
Why doesn't the score render on the page? I've confirmed that the data returns correctly, I just can't seem to access it right.
Here's the console output:
Results is an array.
<h1>{results.map((result) => (result.score)}</h1>

Mock different results of an api called inside react component in jest

I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});

REACT createContext with async function return axios data

I'm creating React context but it returns a promise. In the file playlistcontext.js I've the following code:
import React, { useEffect } from 'react';
import YouTube from '../services/youtube';
const playlistsData = YouTube.getPlaylists();
// console.log(playlistsData);
const PlaylistsDataContext = React.createContext(playlistsData);
const PlaylistsDataProvider = (props) => {
const [playlists, setPlaylists] = React.useState(playlistsData);
useEffect(() =>{
const playlistsData = YouTube.getPlaylists();
console.log(playlistsData);
setPlaylists(playlistsData);
},[])
return <PlaylistsDataContext.Provider value={[playlists, setPlaylists]}>{props.children}</PlaylistsDataContext.Provider>;
}
export {PlaylistsDataContext, PlaylistsDataProvider};
In the file youtube.js, that I use it like a service, I'have the code below. In this function a console.log(result.data) return me the correct data.
import axios from 'axios';
import { YOUTUBE_API } from '../config/config';
function Youtube() {
const handleError = (resp) => {
let message = '';
switch (+resp.status) {
case 401:
message = resp.data.error;
break;
default:
message = 'general error';
}
return message;
}
const getPlaylists = async () => {
try {
const result = await axios.get(YOUTUBE_API + '');
return result.data;
} catch(e) {
return Promise.reject(handleError(e.response));
}
}
return {
getPlaylists
}
}
const ytMethod = Youtube();
export default ytMethod;
then, I have a containers "tutorialcontainer.js" in which I've wrapped a component:
import React, {useState} from 'react';
import { PlaylistsDataProvider } from '../containers/playlistscontext';
import Tutorials from '../components/tutorials';
const TutorialsContainer = (props) => {
return (
<PlaylistsDataProvider>
<Tutorials />
</PlaylistsDataProvider>
);
}
export default TutorialsContainer;
In the last file tutorials.js I have the component. In this file the console.log(playlist) returns me a promise.
import React, {useState, useEffect} from 'react';
import SectionBoxPlaylist from '../components/html_elements/card_playlist';
import Header from '../components/header';
import { PlaylistsDataContext } from '../containers/playlistscontext';
const Tutorials = (props) => {
const [playlists, setPlaylists] = React.useContext(PlaylistsDataContext);
return (
<div className="app-container">
<Header />
<div className="section section-one text-center">
<div className="section-content">
<div className="section-box-items">
{
Object.keys(playlists).map((item) => {
return <SectionBoxPlaylist key={item} id={item} info={playlists[item]} />
})
}
</div>
</div>
</div>
</div>
);
}
export default Tutorials;
Can you help and explain me why?
Thank you!
setPlaylists is called immediately after YouTube.getPlaylists().
useEffect(() => {
const playlistsData = YouTube.getPlaylists();
console.log(playlistsData); // playlistsData is not fetched
setPlaylists(playlistsData);
},[])
You should be able to use .then():
YouTube.getPlaylists().then(response => {
console.log(response);
setPlaylists(response);
});
You can also create async function inside useEffect():
useEffect(() => {
const getYTPlaylist = async () => {
const playlistsData = await YouTube.getPlaylists();
console.log(playlistsData);
setPlaylists(playlistsData);
}
getYTPlaylist();
},[])

Categories