feat(sector-radar): 支持排名列表滚动加载
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const useQuery = vi.hoisted(() => vi.fn())
|
||||
const useInfiniteQuery = vi.hoisted(() => vi.fn())
|
||||
const api = vi.hoisted(() => ({
|
||||
getSectorRadarDates: vi.fn(),
|
||||
getSectorRadarRankings: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({ useQuery }))
|
||||
vi.mock("@tanstack/react-query", () => ({ useInfiniteQuery, useQuery }))
|
||||
vi.mock("./sector-radar.api", () => api)
|
||||
|
||||
import {
|
||||
@@ -32,9 +33,10 @@ describe("sector radar query hooks", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useQuery.mockImplementation((options) => options)
|
||||
useInfiniteQuery.mockImplementation((options) => options)
|
||||
})
|
||||
|
||||
it("keeps every server-affecting filter in the ranking query key", () => {
|
||||
it("keeps filters in one ranking key without splitting cached pages", () => {
|
||||
expect(sectorRadarDatesQueryKey).toEqual(["sectorRadar", "dates"])
|
||||
expect(sectorRadarRankingsQueryKey(query)).toEqual([
|
||||
"sectorRadar",
|
||||
@@ -46,9 +48,11 @@ describe("sector radar query hooks", () => {
|
||||
3,
|
||||
"bottom",
|
||||
"银行",
|
||||
2,
|
||||
50,
|
||||
])
|
||||
expect(
|
||||
sectorRadarRankingsQueryKey({ ...query, page: query.page + 1 }),
|
||||
).toEqual(sectorRadarRankingsQueryKey(query))
|
||||
})
|
||||
|
||||
it("forwards React Query cancellation signals to both adapters", async () => {
|
||||
@@ -57,15 +61,98 @@ describe("sector radar query hooks", () => {
|
||||
const datesOptions = useQuery.mock.calls[0]?.[0] as {
|
||||
queryFn: (context: { signal: AbortSignal }) => unknown
|
||||
}
|
||||
const rankingsOptions = useQuery.mock.calls[1]?.[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({ signal })
|
||||
await rankingsOptions.queryFn({ pageParam: 3, signal })
|
||||
|
||||
expect(api.getSectorRadarDates).toHaveBeenCalledWith(signal)
|
||||
expect(api.getSectorRadarRankings).toHaveBeenCalledWith(query, 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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
type InfiniteData,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { getSectorRadarDates, getSectorRadarRankings } from "./sector-radar.api"
|
||||
import type { RadarRankingsQuery } from "./sector-radar.types"
|
||||
import type {
|
||||
RadarRankingsQuery,
|
||||
RadarRankingsResponse,
|
||||
} from "./sector-radar.types"
|
||||
|
||||
export const sectorRadarDatesQueryKey = ["sectorRadar", "dates"] as const
|
||||
|
||||
@@ -16,7 +23,6 @@ export const sectorRadarRankingsQueryKey = (query: RadarRankingsQuery) =>
|
||||
query.rankChangeDays,
|
||||
query.side,
|
||||
query.search ?? "",
|
||||
query.page,
|
||||
query.pageSize,
|
||||
] as const
|
||||
|
||||
@@ -28,8 +34,33 @@ export function useSectorRadarDates() {
|
||||
}
|
||||
|
||||
export function useSectorRadarRankings(query: RadarRankingsQuery) {
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getSectorRadarRankings(query, signal),
|
||||
return useInfiniteQuery<
|
||||
RadarRankingsResponse,
|
||||
Error,
|
||||
InfiniteData<RadarRankingsResponse>,
|
||||
ReturnType<typeof sectorRadarRankingsQueryKey>,
|
||||
number
|
||||
>({
|
||||
getNextPageParam: (lastPage, pages, lastPageParam) => {
|
||||
const loadedRowCount = pages.reduce(
|
||||
(count, page) => count + page.rows.length,
|
||||
0,
|
||||
)
|
||||
if (
|
||||
lastPage.status !== "success" ||
|
||||
lastPage.rows.length === 0 ||
|
||||
lastPage.page !== lastPageParam ||
|
||||
loadedRowCount >= lastPage.total
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const nextPageParam = lastPageParam + 1
|
||||
return Number.isSafeInteger(nextPageParam) ? nextPageParam : undefined
|
||||
},
|
||||
initialPageParam: 1,
|
||||
queryFn: ({ pageParam, signal }) =>
|
||||
getSectorRadarRankings({ ...query, page: pageParam }, signal),
|
||||
queryKey: sectorRadarRankingsQueryKey(query),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user