2026-08-29 20:14:42 +08:00
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
|
|
|
|
|
|
const useQuery = vi.hoisted(() => vi.fn())
|
2026-09-01 11:16:18 +08:00
|
|
|
const useInfiniteQuery = vi.hoisted(() => vi.fn())
|
2026-08-29 20:14:42 +08:00
|
|
|
const api = vi.hoisted(() => ({
|
2026-09-06 16:06:17 +08:00
|
|
|
getSectorRadarHistory: vi.fn(),
|
|
|
|
|
getSectorRadarDetail: vi.fn(),
|
2026-08-29 20:14:42 +08:00
|
|
|
getSectorRadarDates: vi.fn(),
|
|
|
|
|
getSectorRadarRankings: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
vi.mock("@tanstack/react-query", () => ({ useInfiniteQuery, useQuery }))
|
2026-08-29 20:14:42 +08:00
|
|
|
vi.mock("./sector-radar.api", () => api)
|
|
|
|
|
|
|
|
|
|
import {
|
2026-09-06 16:06:17 +08:00
|
|
|
sectorRadarSectorQueryKey,
|
|
|
|
|
useSectorRadarDetail,
|
|
|
|
|
useSectorRadarHistory,
|
2026-08-29 20:14:42 +08:00
|
|
|
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)
|
2026-09-01 11:16:18 +08:00
|
|
|
useInfiniteQuery.mockImplementation((options) => options)
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-06 16:06:17 +08:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
it("keeps filters in one ranking key without splitting cached pages", () => {
|
2026-08-29 20:14:42 +08:00
|
|
|
expect(sectorRadarDatesQueryKey).toEqual(["sectorRadar", "dates"])
|
|
|
|
|
expect(sectorRadarRankingsQueryKey(query)).toEqual([
|
|
|
|
|
"sectorRadar",
|
|
|
|
|
"rankings",
|
|
|
|
|
"2026-08-28",
|
|
|
|
|
"industry",
|
|
|
|
|
"rank_change",
|
|
|
|
|
"ratio",
|
|
|
|
|
3,
|
|
|
|
|
"bottom",
|
|
|
|
|
"银行",
|
|
|
|
|
50,
|
|
|
|
|
])
|
2026-09-01 11:16:18 +08:00
|
|
|
expect(
|
|
|
|
|
sectorRadarRankingsQueryKey({ ...query, page: query.page + 1 }),
|
|
|
|
|
).toEqual(sectorRadarRankingsQueryKey(query))
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-09-01 11:16:18 +08:00
|
|
|
const rankingsOptions = useInfiniteQuery.mock.calls[0]?.[0] as {
|
|
|
|
|
queryFn: (context: { pageParam: number; signal: AbortSignal }) => unknown
|
2026-08-29 20:14:42 +08:00
|
|
|
}
|
|
|
|
|
const signal = new AbortController().signal
|
|
|
|
|
|
|
|
|
|
await datesOptions.queryFn({ signal })
|
2026-09-01 11:16:18 +08:00
|
|
|
await rankingsOptions.queryFn({ pageParam: 3, signal })
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
expect(api.getSectorRadarDates).toHaveBeenCalledWith(signal)
|
2026-09-01 11:16:18 +08:00
|
|
|
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()
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
})
|