/** * 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"); }); });