193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
/**
|
||
* useCompareData Hook 单元测试
|
||
*/
|
||
|
||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||
import { renderHook, waitFor, act } from "@testing-library/react";
|
||
import { SWRConfig } from "swr";
|
||
import type { ReactNode } from "react";
|
||
|
||
vi.mock("@/lib/api/client", () => ({
|
||
fetchWithAuth: vi.fn(),
|
||
}));
|
||
|
||
import { fetchWithAuth } from "@/lib/api/client";
|
||
const mockFetchWithAuth = vi.mocked(fetchWithAuth);
|
||
|
||
vi.mock("next-auth/react", () => ({
|
||
useSession: vi.fn(() => ({
|
||
data: { accessToken: "test-token" },
|
||
status: "authenticated",
|
||
})),
|
||
}));
|
||
|
||
import { useCompareData } from "@/lib/hooks/use-compare-data";
|
||
import type { BrandListResponse, CompareResponse } from "@/types/brand";
|
||
|
||
const noRetryOptions = { shouldRetryOnError: false };
|
||
|
||
/** 使用独立 SWR 缓存的 wrapper,避免测试间缓存冲突 */
|
||
function createWrapper() {
|
||
return ({ children }: { children: ReactNode }) =>
|
||
SWRConfig({
|
||
value: { provider: () => new Map() },
|
||
children,
|
||
});
|
||
}
|
||
|
||
describe("useCompareData", () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
const mockBrandsResponse: BrandListResponse = {
|
||
items: [
|
||
{
|
||
id: "brand-1",
|
||
name: "测试品牌",
|
||
aliases: [],
|
||
platforms: ["wenxin"],
|
||
frequency: "weekly",
|
||
status: "active",
|
||
score: 80,
|
||
last_queried_at: null,
|
||
next_query_at: null,
|
||
created_at: "2024-01-01",
|
||
},
|
||
],
|
||
total: 1,
|
||
};
|
||
|
||
const mockCompareResponse: CompareResponse = {
|
||
brand_id: "brand-1",
|
||
brand_name: "测试品牌",
|
||
items: [
|
||
{
|
||
entity_id: "brand-1",
|
||
entity_name: "测试品牌",
|
||
entity_type: "brand",
|
||
mention_rate_score: 70,
|
||
sov_score: 65,
|
||
quality_score: 80,
|
||
overall_score: 75,
|
||
citation_count: 100,
|
||
dimensions: [],
|
||
overall_trend: "up",
|
||
overall_trend_value: 5,
|
||
},
|
||
],
|
||
radar_data: [],
|
||
};
|
||
|
||
it("应返回品牌列表数据", async () => {
|
||
mockFetchWithAuth.mockImplementation((url: string) => {
|
||
if (url.includes("/api/v1/brands")) return Promise.resolve(mockBrandsResponse);
|
||
return Promise.resolve({});
|
||
});
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.brands).toEqual(mockBrandsResponse.items);
|
||
});
|
||
});
|
||
|
||
it("selectedBrandId 为空时应暂停对比数据请求", () => {
|
||
mockFetchWithAuth.mockResolvedValue(mockBrandsResponse);
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
expect(result.current.compareData).toBeUndefined();
|
||
});
|
||
|
||
it("selectedBrandId 存在时应获取对比数据", async () => {
|
||
mockFetchWithAuth.mockImplementation((url: string) => {
|
||
if (url.includes("/compare")) return Promise.resolve(mockCompareResponse);
|
||
return Promise.resolve(mockBrandsResponse);
|
||
});
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ initialBrandId: "brand-1", swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.compareData).toEqual(mockCompareResponse);
|
||
});
|
||
});
|
||
|
||
it("应正确暴露 loading 状态", async () => {
|
||
mockFetchWithAuth.mockImplementation((url: string) => {
|
||
if (url.includes("/compare")) return Promise.resolve(mockCompareResponse);
|
||
return Promise.resolve(mockBrandsResponse);
|
||
});
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ initialBrandId: "brand-1", swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.isLoading).toBe(false);
|
||
});
|
||
});
|
||
|
||
it("应正确暴露 error 状态", async () => {
|
||
mockFetchWithAuth.mockRejectedValue(new Error("网络错误"));
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.error).toBeDefined();
|
||
expect(result.current.error?.message).toBe("网络错误");
|
||
});
|
||
});
|
||
|
||
it("setSelectedBrandId 应更新选中品牌", async () => {
|
||
mockFetchWithAuth.mockImplementation((url: string) => {
|
||
if (url.includes("/compare")) return Promise.resolve(mockCompareResponse);
|
||
return Promise.resolve(mockBrandsResponse);
|
||
});
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.brands.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
act(() => {
|
||
result.current.setSelectedBrandId("brand-1");
|
||
});
|
||
|
||
expect(result.current.selectedBrandId).toBe("brand-1");
|
||
});
|
||
|
||
it("应提供 refresh 方法刷新数据", async () => {
|
||
mockFetchWithAuth.mockResolvedValue(mockBrandsResponse);
|
||
|
||
const { result } = renderHook(
|
||
() => useCompareData({ swrOptions: noRetryOptions }),
|
||
{ wrapper: createWrapper() }
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.isLoading).toBe(false);
|
||
});
|
||
|
||
expect(typeof result.current.refreshBrands).toBe("function");
|
||
expect(typeof result.current.refreshCompare).toBe("function");
|
||
});
|
||
});
|