import { beforeEach, describe, expect, it, vi } from "vitest" const useQuery = vi.hoisted(() => vi.fn()) const useInfiniteQuery = vi.hoisted(() => vi.fn()) const api = vi.hoisted(() => ({ getSectorRadarHistory: vi.fn(), getSectorRadarDetail: vi.fn(), getSectorRadarDates: vi.fn(), getSectorRadarRankings: vi.fn(), })) vi.mock("@tanstack/react-query", () => ({ useInfiniteQuery, useQuery })) vi.mock("./sector-radar.api", () => api) import { sectorRadarSectorQueryKey, useSectorRadarDetail, useSectorRadarHistory, sectorRadarDatesQueryKey, sectorRadarRankingsQueryKey, useSectorRadarDates, useSectorRadarRankings, } from "./sector-radar.query" const query = { tradeDate: "2026-08-28", sectorType: "industry" as const, view: "rank_change" as const, rankChangeMetric: "ratio" as const, rankChangeDays: 3, side: "bottom" as const, search: "银行", page: 2, pageSize: 50, } describe("sector radar query hooks", () => { beforeEach(() => { vi.clearAllMocks() useQuery.mockImplementation((options) => options) useInfiniteQuery.mockImplementation((options) => options) }) it("isolates sector resources by type, code and date and forwards cancellation", () => { const sector = { sectorType: "concept" as const, sectorCode: "BK01", tradeDate: "2026-08-28", } const signal = new AbortController().signal useSectorRadarDetail(sector) const options = useQuery.mock.calls.at(-1)?.[0] expect(options.queryKey).toEqual([ "sectorRadar", "detail", "concept", "BK01", "2026-08-28", ]) expect(options.placeholderData).toBeUndefined() options.queryFn({ signal }) expect(api.getSectorRadarDetail).toHaveBeenCalledWith(sector, signal) expect( sectorRadarSectorQueryKey("detail", { ...sector, tradeDate: "2026-08-31", }), ).not.toEqual(options.queryKey) expect( sectorRadarSectorQueryKey("detail", { ...sector, sectorType: "industry", }), ).not.toEqual(options.queryKey) useSectorRadarHistory(sector) useQuery.mock.calls.at(-1)?.[0].queryFn({ signal }) expect(api.getSectorRadarHistory).toHaveBeenCalledWith(sector, signal) }) it("keeps filters in one ranking key without splitting cached pages", () => { expect(sectorRadarDatesQueryKey).toEqual(["sectorRadar", "dates"]) expect(sectorRadarRankingsQueryKey(query)).toEqual([ "sectorRadar", "rankings", "2026-08-28", "industry", "rank_change", "ratio", 3, "bottom", "银行", 50, ]) expect( sectorRadarRankingsQueryKey({ ...query, page: query.page + 1 }), ).toEqual(sectorRadarRankingsQueryKey(query)) }) it("forwards React Query cancellation signals to both adapters", async () => { useSectorRadarDates() useSectorRadarRankings(query) const datesOptions = useQuery.mock.calls[0]?.[0] as { queryFn: (context: { signal: AbortSignal }) => unknown } const rankingsOptions = useInfiniteQuery.mock.calls[0]?.[0] as { queryFn: (context: { pageParam: number; signal: AbortSignal }) => unknown } const signal = new AbortController().signal await datesOptions.queryFn({ signal }) await rankingsOptions.queryFn({ pageParam: 3, signal }) expect(api.getSectorRadarDates).toHaveBeenCalledWith(signal) expect(api.getSectorRadarRankings).toHaveBeenCalledWith( { ...query, page: 3 }, signal, ) }) it("starts from page one and stops after loaded rows cover total", () => { useSectorRadarRankings(query) const rankingsOptions = useInfiniteQuery.mock.calls[0]?.[0] as { getNextPageParam: ( lastPage: { page: number; rows: unknown[]; total: number }, pages: { page: number; rows: unknown[]; total: number }[], lastPageParam: number, ) => number | undefined initialPageParam: number } const firstPage = { page: 1, rows: Array(50).fill(null), status: "success", total: 75, } const secondPage = { page: 2, rows: Array(25).fill(null), status: "success", total: 75, } expect(rankingsOptions.initialPageParam).toBe(1) expect(rankingsOptions.getNextPageParam(firstPage, [firstPage], 1)).toBe(2) expect( rankingsOptions.getNextPageParam(secondPage, [firstPage, secondPage], 2), ).toBeUndefined() }) it("stops when a page is empty or does not match its requested page param", () => { useSectorRadarRankings(query) const rankingsOptions = useInfiniteQuery.mock.calls[0]?.[0] as { getNextPageParam: ( lastPage: { page: number rows: unknown[] status: "success" | "no_data" total: number }, pages: { page: number rows: unknown[] status: "success" | "no_data" total: number }[], lastPageParam: number, ) => number | undefined } const firstPage = { page: 1, rows: Array(50).fill(null), status: "success" as const, total: 75, } const emptyPage = { page: 2, rows: [], status: "success" as const, total: 75, } const mismatchedPage = { page: 1, rows: Array(25).fill(null), status: "success" as const, total: 75, } expect( rankingsOptions.getNextPageParam(emptyPage, [firstPage, emptyPage], 2), ).toBeUndefined() expect( rankingsOptions.getNextPageParam( mismatchedPage, [firstPage, mismatchedPage], 2, ), ).toBeUndefined() }) })