everyone. I’m preparing a test for a page on my website in NextJS, and in that page I’m supposed to be doing a fetch call to an /api route by means of a hook. This route executes a query that fetches data from a SQLite database, which is then returned to the page to populate it with the data. My page looks like this:
'use client';
import { useParams } from 'next/navigation';
import AssetButton from '@/components/AssetButton';
import AssetInfo from '@/components/AssetInfo';
import AssetLogo from '@/components/AssetLogo';
import AssetScreenshot from '@/components/AssetScreenshot';
import Details from '@/components/Details';
import Loader from '@/components/Loader';
import Page from '@/components/Page';
import Text from '@/components/Text';
import useFetchData from '@/hooks/useFetchData';
import { Asset } from '@/utils/types';
export default function ProgramDetails() {
const params = useParams();
const { id } = params;
const {
data: program,
loading,
error,
} = useFetchData<Asset>(`/api/programs/${id}`);
const errorData = <p>{error}</p>;
const isDownload = !program?.link?.includes('https://');
return (
<Page>
{loading ? (
<Loader />
) : error ? (
errorData
) : (
<>
<AssetLogo icon={program!.icon} />
<Text content={program!.name?.toUpperCase()} type="title" />
<Text content={program!.description} type="body" />
<AssetInfo>
<AssetScreenshot file={program!.icon} />
<Details details={program!.details?.split(';')} />
</AssetInfo>
<AssetButton isDownload={isDownload} link={program!.link} />
</>
)}
</Page>
);
}
Where I use the useFetchData hook to be able to tell when the api call is loading and when it gets data, so I can render a loader while the data is fetched. The hook looks like this:
import { useState, useEffect } from 'react';
const useFetchData = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
fetch(url)
.then(async (res) => {
const data = await res.json();
if (data) {
setData(data);
} else {
setError('An unknown error has occurred');
}
})
.catch((err) => {
setError(err.message);
})
.finally(() => {
setLoading(false);
});
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useFetchData;
and the api route /api/programs/[id] looks like this:
import { NextResponse } from 'next/server';
import openDb from '@/utils/db';
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
const db = openDb();
const { id } = params;
const program = db.prepare('SELECT * FROM programs WHERE id = ?').get(id);
if (!program) {
return NextResponse.json({ error: 'Program not found' }, { status: 404 });
}
return NextResponse.json(program);
}
so the test is meant to mock a fetch call to return dummy data and check that the dummy data renders:
import { useParams } from 'next/navigation';
import { render, screen, waitFor } from '@testing-library/react';
import ProgramDetails from '@/app/programs/[id]/page';
import { ViewportProvider } from '@/context/ViewportContext';
const mockProgramData = [
{
id: 1,
name: 'Program 1',
icon: 'icon1.png',
description: 'Description for Program 1',
details: ['detail 1', 'detail 2'],
link: '/downloads/test.zip',
},
];
jest.mock('next/navigation', () => ({
useParams: jest.fn().mockReturnValue({ id: '1' }),
}));
describe('Program Details Page', () => {
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockProgramData),
}),
) as jest.Mock;
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders the program title, description, and details correctly', async () => {
render(
<ViewportProvider>
<ProgramDetails />
</ViewportProvider>,
);
await waitFor(() => {
expect(screen.queryByTestId('triple-spinner')).not.toBeInTheDocument();
});
const title = await screen.findByTestId('text-title');
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent(mockProgramData[0].name);
const body = await screen.findByTestId('text-body');
expect(body).toBeInTheDocument();
expect(body).toHaveTextContent(mockProgramData[0].description);
await waitFor(() => {
mockProgramData[0].details.forEach((currDetail) => {
expect(screen.getByText(currDetail)).toBeInTheDocument();
expect(screen.getByText(currDetail)).toBeInTheDocument();
});
});
});
});
at first I was assuming that, waiting for the loader’s test id to not render would be enough to ensure the data is rendering. Just in case the loader looks like this:
import styles from './Loader.module.css';
const Loader: React.FC = () => {
return (
<div data-testid="loader" id={styles.loader}>
<div
className={styles['triple-spinner']}
data-testid="triple-spinner"
></div>
</div>
);
};
export default Loader;
Then, I would get the other elements by test id exist, and check if the text has rendered and matches. The Text component that has these ids is this:
import styles from './Text.module.css';
interface TextProps {
content: string;
type: 'body' | 'card' | 'hero' | 'modal' | 'title';
}
const Text: React.FC<TextProps> = ({ content, type }) => {
return type === 'title' ? (
<h3 className={styles[type]} data-testid="text-title">
{content}
</h3>
) : (
<p className={styles[type]} data-testid="text-body">
{content}
</p>
);
};
export default Text;
But as I run the test, I get this error:
expect(element).toHaveTextContent()
Expected element to have text content:
Program 1
Received:
45 | const title = await screen.findByTestId('text-title');
46 | expect(title).toBeInTheDocument();
> 47 | expect(title).toHaveTextContent(mockProgramData[0].name);
| ^
48 |
49 | const body = await screen.findByTestId('text-body');
```
Since nothing shows, I tried printing the results of the fetch call in the hook, and I saw two iterations: one where the returned data is null, and another with the test data. So what I appear to need is to be able to skip that first call to ensure I can work with the actual returned data, but how can I accomplish that?