我正在 NextJS 中为我的网站上的页面准备测试,在该页面中我应该通过钩子对 /api 路由进行 fetch 调用。该路由执行一个从 SQLite 数据库获取数据的查询,然后将其返回到页面以使用数据填充该页面。我的页面如下所示:
'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>
);
}
我使用
useFetchData
钩子来判断 api 调用何时加载以及何时获取数据,这样我就可以在获取数据时渲染加载器。钩子看起来像这样:
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;
API 路由
/api/programs/[id]
看起来像这样:
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);
}
因此测试旨在模拟 fetch 调用以返回虚拟数据并检查虚拟数据是否呈现:
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();
});
});
});
});
起初我假设,等待加载程序的测试 ID 不渲染就足以确保数据渲染。以防万一加载器看起来像这样:
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;
然后,我将通过测试 id 获取其他元素是否存在,并检查文本是否已渲染并匹配。具有这些 id 的文本组件是这样的:
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;
但是当我运行测试时,我收到此错误:
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');
由于没有任何显示,我尝试在钩子中打印 fetch 调用的结果,我看到了两次迭代:一次返回的数据为空,另一次返回测试数据。因此,我似乎需要能够跳过第一个调用,以确保我可以使用实际返回的数据,但我怎样才能实现这一点呢?