2026-08-29 20:14:42 +08:00
|
|
|
import { fireEvent, render, screen } from "@testing-library/react"
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
RadarDatesResponse,
|
|
|
|
|
RadarRankingsResponse,
|
|
|
|
|
SectorRadarRouteSearch,
|
|
|
|
|
} from "../api/sector-radar.types"
|
|
|
|
|
|
|
|
|
|
import { SectorRadarPage } from "./sector-radar-page"
|
|
|
|
|
|
|
|
|
|
const navigate = vi.hoisted(() => vi.fn())
|
|
|
|
|
const useSectorRadarDates = vi.fn()
|
|
|
|
|
const useSectorRadarRankings = vi.fn()
|
2026-09-06 16:06:17 +08:00
|
|
|
const useSectorRadarHistory = vi.fn()
|
2026-08-29 20:14:42 +08:00
|
|
|
const refetchDates = vi.fn()
|
|
|
|
|
const refetchRankings = vi.fn()
|
2026-09-01 11:16:18 +08:00
|
|
|
const fetchNextPage = vi.fn()
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
let routeSearch: SectorRadarRouteSearch
|
|
|
|
|
|
|
|
|
|
vi.mock("@tanstack/react-router", () => ({
|
|
|
|
|
useNavigate: () => navigate,
|
|
|
|
|
useSearch: () => routeSearch,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
vi.mock("@/features/sector-radar/api/sector-radar.query", () => ({
|
2026-09-06 16:06:17 +08:00
|
|
|
useSectorRadarHistory: (...args: unknown[]) => useSectorRadarHistory(...args),
|
2026-08-29 20:14:42 +08:00
|
|
|
useSectorRadarDates: () => useSectorRadarDates(),
|
|
|
|
|
useSectorRadarRankings: (...args: unknown[]) =>
|
|
|
|
|
useSectorRadarRankings(...args),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const successPublication = {
|
|
|
|
|
publication_id: "publication-success",
|
|
|
|
|
target_trade_date: "2026-08-28",
|
|
|
|
|
status: "success" as const,
|
|
|
|
|
source_version: "tushare-pro-v1",
|
|
|
|
|
universe_version: "eastmoney-dc-v1",
|
|
|
|
|
metric_versions: ["zhixing_amount_net_bn_v1"],
|
|
|
|
|
input_hash: "a".repeat(64),
|
|
|
|
|
coverage: 0.99,
|
|
|
|
|
started_at: "2026-08-28T08:00:00Z",
|
|
|
|
|
finished_at: "2026-08-28T08:05:00Z",
|
|
|
|
|
error_summary: null,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const datesResponse: RadarDatesResponse = {
|
|
|
|
|
status: "success",
|
|
|
|
|
available_dates: ["2026-08-28", "2026-08-27"],
|
|
|
|
|
current_attempt: successPublication,
|
|
|
|
|
last_good: successPublication,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rankingsResponse: RadarRankingsResponse = {
|
|
|
|
|
status: "success",
|
|
|
|
|
requested_trade_date: null,
|
|
|
|
|
sector_type: "concept",
|
|
|
|
|
view: "amount",
|
|
|
|
|
rank_change_metric: "amount",
|
|
|
|
|
rank_change_days: 1,
|
|
|
|
|
side: "all",
|
|
|
|
|
search: null,
|
|
|
|
|
publication: successPublication,
|
|
|
|
|
definition: {
|
|
|
|
|
metric_kind: "amount",
|
|
|
|
|
metric_version: "zhixing_amount_net_bn_v1",
|
|
|
|
|
label: "主力净流入(知行独立实现)",
|
|
|
|
|
unit: "CNY_100M",
|
|
|
|
|
implementation_kind: "independent",
|
|
|
|
|
disclaimer: "知行独立实现,非 OneChartLab 原站公式。",
|
|
|
|
|
},
|
|
|
|
|
page: 1,
|
|
|
|
|
page_size: 20,
|
|
|
|
|
total: 2,
|
|
|
|
|
rows: [
|
|
|
|
|
{
|
|
|
|
|
trade_date: "2026-08-28",
|
|
|
|
|
sector_type: "concept",
|
|
|
|
|
sector_code: "BK0001.DC",
|
|
|
|
|
sector_name: "机器人",
|
|
|
|
|
metric_kind: "amount",
|
|
|
|
|
metric_version: "zhixing_amount_net_bn_v1",
|
|
|
|
|
implementation_kind: "independent",
|
|
|
|
|
unit: "CNY_100M",
|
|
|
|
|
metric_value: 12.5,
|
|
|
|
|
quality: "available",
|
|
|
|
|
member_count: 20,
|
|
|
|
|
valid_sample_count: 19,
|
|
|
|
|
membership_coverage: 1,
|
|
|
|
|
moneyflow_coverage: 0.95,
|
|
|
|
|
rank_position: 1,
|
|
|
|
|
rank_percentile: 100,
|
|
|
|
|
rank_change_days: 1,
|
|
|
|
|
rank_change: 3,
|
2026-09-06 16:06:17 +08:00
|
|
|
pct_change: -1.25,
|
|
|
|
|
daily_net_amount_yuan: 1250000000,
|
|
|
|
|
daily_ratio: -0.02,
|
|
|
|
|
on_list_count: 3,
|
|
|
|
|
history_available_days: 10,
|
2026-08-29 20:14:42 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
trade_date: "2026-08-28",
|
|
|
|
|
sector_type: "concept",
|
|
|
|
|
sector_code: "BK0002.DC",
|
|
|
|
|
sector_name: "低空经济",
|
|
|
|
|
metric_kind: "amount",
|
|
|
|
|
metric_version: "zhixing_amount_net_bn_v1",
|
|
|
|
|
implementation_kind: "independent",
|
|
|
|
|
unit: "CNY_100M",
|
|
|
|
|
metric_value: null,
|
|
|
|
|
quality: "available_limited_sample",
|
|
|
|
|
member_count: 4,
|
|
|
|
|
valid_sample_count: 3,
|
|
|
|
|
membership_coverage: 1,
|
|
|
|
|
moneyflow_coverage: 0.75,
|
|
|
|
|
rank_position: null,
|
|
|
|
|
rank_percentile: null,
|
|
|
|
|
rank_change_days: 1,
|
|
|
|
|
rank_change: null,
|
2026-09-06 16:06:17 +08:00
|
|
|
pct_change: null,
|
|
|
|
|
daily_net_amount_yuan: null,
|
|
|
|
|
daily_ratio: null,
|
|
|
|
|
on_list_count: null,
|
|
|
|
|
history_available_days: 0,
|
2026-08-29 20:14:42 +08:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
const bottomRankingsResponse: RadarRankingsResponse = {
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
side: "bottom",
|
|
|
|
|
rows: rankingsResponse.rows.map((row, index) => ({
|
|
|
|
|
...row,
|
|
|
|
|
sector_code: `BOTTOM-${index + 1}`,
|
|
|
|
|
sector_name: index === 0 ? "新能源" : "医药商业",
|
|
|
|
|
metric_value: index === 0 ? -8.4 : null,
|
|
|
|
|
rank_position: index + 1,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
function rankingQueryResult(overrides: Record<string, unknown> = {}) {
|
|
|
|
|
return {
|
|
|
|
|
data: { pageParams: [1], pages: [rankingsResponse] },
|
|
|
|
|
fetchNextPage,
|
|
|
|
|
hasNextPage: false,
|
|
|
|
|
isError: false,
|
|
|
|
|
isFetchNextPageError: false,
|
|
|
|
|
isFetching: false,
|
|
|
|
|
isFetchingNextPage: false,
|
|
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchRankings,
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 20:14:42 +08:00
|
|
|
describe("SectorRadarPage", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
routeSearch = {
|
|
|
|
|
tradeDate: undefined,
|
|
|
|
|
sectorType: "concept",
|
|
|
|
|
view: "amount",
|
|
|
|
|
rankChangeMetric: "amount",
|
|
|
|
|
rankChangeDays: 1,
|
|
|
|
|
side: "all",
|
|
|
|
|
search: "",
|
|
|
|
|
page: 1,
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
}
|
|
|
|
|
useSectorRadarDates.mockReturnValue({
|
|
|
|
|
data: datesResponse,
|
|
|
|
|
isError: false,
|
|
|
|
|
isFetching: false,
|
|
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchDates,
|
|
|
|
|
})
|
2026-09-01 11:16:18 +08:00
|
|
|
useSectorRadarRankings.mockReturnValue(rankingQueryResult())
|
2026-09-06 16:06:17 +08:00
|
|
|
useSectorRadarHistory.mockReturnValue({
|
|
|
|
|
isPending: false,
|
|
|
|
|
isError: false,
|
|
|
|
|
data: { status: "no_data" },
|
|
|
|
|
})
|
2026-08-29 20:14:42 +08:00
|
|
|
navigate.mockReset()
|
|
|
|
|
refetchDates.mockReset()
|
|
|
|
|
refetchRankings.mockReset()
|
2026-09-01 11:16:18 +08:00
|
|
|
fetchNextPage.mockReset()
|
|
|
|
|
fetchNextPage.mockResolvedValue(undefined)
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
it("renders ranking rows without the description card or pagination", () => {
|
2026-08-29 20:14:42 +08:00
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("+12.5 亿元")[0]).toBeInTheDocument()
|
|
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
2026-09-06 16:06:17 +08:00
|
|
|
expect(screen.queryByText("19/20")).not.toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText("BK0001.DC")).not.toBeInTheDocument()
|
2026-09-01 14:57:27 +08:00
|
|
|
expect(
|
2026-09-06 16:06:17 +08:00
|
|
|
screen.queryByRole("columnheader", { name: "排名百分位" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("columnheader", { name: "样本" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.getAllByRole("columnheader", { name: "涨跌幅" }),
|
2026-09-01 14:57:27 +08:00
|
|
|
).toHaveLength(2)
|
2026-09-06 16:06:17 +08:00
|
|
|
expect(
|
|
|
|
|
screen.getAllByRole("columnheader", { name: "单日流入率" }),
|
|
|
|
|
).toHaveLength(2)
|
|
|
|
|
expect(screen.getAllByText("-2%")).toHaveLength(2)
|
2026-09-01 14:57:27 +08:00
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("columnheader", { name: "资金覆盖率" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("columnheader", { name: "质量" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText("样本有限")).not.toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText("95%")).not.toBeInTheDocument()
|
2026-09-01 11:16:18 +08:00
|
|
|
expect(screen.queryByText("收盘后板块资金排名")).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByText("知行独立实现,非 OneChartLab 原站公式。"),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByText("zhixing_amount_net_bn_v1"),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("button", { name: "第 1 页" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("combobox", { name: "每页数量" }),
|
|
|
|
|
).not.toBeInTheDocument()
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-06 16:06:17 +08:00
|
|
|
it("mirrors ratio columns and expands only the selected sector history, resetting on date change", () => {
|
|
|
|
|
routeSearch = { ...routeSearch, view: "ratio" }
|
|
|
|
|
const ratioPage = {
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
view: "ratio",
|
|
|
|
|
rows: rankingsResponse.rows.map((row) => ({
|
|
|
|
|
...row,
|
|
|
|
|
unit: "ratio",
|
|
|
|
|
metric_value: -0.01,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({ data: { pages: [ratioPage], pageParams: [1] } }),
|
|
|
|
|
)
|
|
|
|
|
const { rerender } = render(<SectorRadarPage />)
|
|
|
|
|
expect(screen.getAllByRole("columnheader", { name: "在榜" })).toHaveLength(
|
|
|
|
|
2,
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getAllByText("-1%")).toHaveLength(4)
|
|
|
|
|
const trigger = screen.getAllByRole("button", {
|
|
|
|
|
name: "机器人展开在榜历史",
|
|
|
|
|
})[0]!
|
|
|
|
|
fireEvent.click(trigger)
|
|
|
|
|
expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
|
|
|
expect(useSectorRadarHistory).toHaveBeenLastCalledWith({
|
|
|
|
|
sectorType: "concept",
|
|
|
|
|
sectorCode: "BK0001.DC",
|
|
|
|
|
tradeDate: "2026-08-28",
|
|
|
|
|
})
|
|
|
|
|
expect(screen.getByText("暂无在榜历史数据。")).toBeInTheDocument()
|
|
|
|
|
fireEvent.click(trigger)
|
|
|
|
|
expect(screen.queryByText("暂无在榜历史数据。")).not.toBeInTheDocument()
|
|
|
|
|
fireEvent.click(trigger)
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
data: {
|
|
|
|
|
pages: [
|
|
|
|
|
{
|
|
|
|
|
...ratioPage,
|
|
|
|
|
publication: {
|
|
|
|
|
...successPublication,
|
|
|
|
|
publication_id: "next-day",
|
|
|
|
|
},
|
|
|
|
|
rows: ratioPage.rows.map((row) => ({
|
|
|
|
|
...row,
|
|
|
|
|
trade_date: "2026-08-31",
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pageParams: [1],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
rerender(<SectorRadarPage />)
|
|
|
|
|
expect(screen.queryByText("暂无在榜历史数据。")).not.toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.getAllByRole("button", { name: "机器人展开在榜历史" })[0],
|
|
|
|
|
).toHaveAttribute("aria-expanded", "false")
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
it("resets filters to the first batch in router search state", () => {
|
|
|
|
|
routeSearch = { ...routeSearch, page: 3 }
|
2026-08-29 20:14:42 +08:00
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByRole("searchbox", { name: "搜索板块" }), {
|
|
|
|
|
target: { value: "机器人" },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const searchCall = navigate.mock.calls[0]?.[0].search as (
|
|
|
|
|
previous: SectorRadarRouteSearch,
|
|
|
|
|
) => SectorRadarRouteSearch
|
|
|
|
|
expect(searchCall({ ...routeSearch, page: 3 })).toMatchObject({
|
|
|
|
|
page: 1,
|
|
|
|
|
search: "机器人",
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
it("renders accessible tabs, writes tab state to the URL, and queries both sides", () => {
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByRole("tablist", { name: "板块类型" }),
|
|
|
|
|
).toBeInTheDocument()
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByRole("tablist", { name: "指标视角" }),
|
|
|
|
|
).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole("tab", { name: "单日净额" })).toHaveAttribute(
|
|
|
|
|
"aria-selected",
|
|
|
|
|
"true",
|
|
|
|
|
)
|
|
|
|
|
expect(useSectorRadarRankings).toHaveBeenNthCalledWith(
|
|
|
|
|
1,
|
|
|
|
|
expect.objectContaining({ side: "top", view: "amount" }),
|
|
|
|
|
)
|
|
|
|
|
expect(useSectorRadarRankings).toHaveBeenNthCalledWith(
|
|
|
|
|
2,
|
|
|
|
|
expect.objectContaining({ side: "bottom", view: "amount" }),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("tab", { name: "行业" }))
|
|
|
|
|
const searchCall = navigate.mock.calls[0]?.[0].search as (
|
|
|
|
|
previous: SectorRadarRouteSearch,
|
|
|
|
|
) => SectorRadarRouteSearch
|
|
|
|
|
expect(searchCall({ ...routeSearch, page: 4 })).toMatchObject({
|
|
|
|
|
page: 1,
|
|
|
|
|
sectorType: "industry",
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:16:18 +08:00
|
|
|
it("loads the next page once when repeated scroll events reach the bottom", () => {
|
|
|
|
|
fetchNextPage.mockReturnValue(new Promise(() => undefined))
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({ hasNextPage: true }),
|
|
|
|
|
)
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
const scrollRegion = screen.getByRole("region", {
|
|
|
|
|
name: "板块资金排名滚动区域",
|
|
|
|
|
})
|
|
|
|
|
Object.defineProperties(scrollRegion, {
|
|
|
|
|
clientHeight: { configurable: true, value: 300 },
|
|
|
|
|
scrollHeight: { configurable: true, value: 1_000 },
|
|
|
|
|
scrollTop: { configurable: true, value: 550 },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
fireEvent.scroll(scrollRegion)
|
|
|
|
|
fireEvent.scroll(scrollRegion)
|
|
|
|
|
|
|
|
|
|
expect(fetchNextPage).toHaveBeenCalledOnce()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("announces that the next page is loading without hiding loaded rows", () => {
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
hasNextPage: true,
|
|
|
|
|
isFetching: true,
|
|
|
|
|
isFetchingNextPage: true,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent("正在加载更多板块")
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
2026-09-01 11:16:18 +08:00
|
|
|
expect(screen.queryByText("雷达数据正在刷新")).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("merges rows from every loaded page", () => {
|
|
|
|
|
const firstPage = { ...rankingsResponse, total: 3 }
|
|
|
|
|
const nextPage = {
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
page: 2,
|
|
|
|
|
total: 3,
|
|
|
|
|
rows: [
|
|
|
|
|
{
|
|
|
|
|
...rankingsResponse.rows[0],
|
|
|
|
|
sector_code: "BK0003.DC",
|
|
|
|
|
sector_name: "商业航天",
|
|
|
|
|
rank_position: 3,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
data: { pageParams: [1, 2], pages: [firstPage, nextPage] },
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
|
|
|
|
expect(screen.getAllByText("低空经济")[0]).toBeInTheDocument()
|
|
|
|
|
expect(screen.getAllByText("商业航天")[0]).toBeInTheDocument()
|
2026-09-01 11:16:18 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("keeps loaded rows and offers retry when loading more fails", () => {
|
|
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
hasNextPage: true,
|
|
|
|
|
isError: true,
|
|
|
|
|
isFetchNextPageError: true,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
2026-09-01 11:16:18 +08:00
|
|
|
expect(screen.getByRole("alert")).toHaveTextContent(
|
2026-09-01 12:14:02 +08:00
|
|
|
"强榜加载更多失败,弱榜加载更多失败,已加载结果仍保留",
|
2026-09-01 11:16:18 +08:00
|
|
|
)
|
|
|
|
|
expect(screen.queryByText("雷达数据刷新失败")).not.toBeInTheDocument()
|
2026-09-01 12:14:02 +08:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: "重试强榜" }))
|
2026-09-01 11:16:18 +08:00
|
|
|
expect(fetchNextPage).toHaveBeenCalledOnce()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
it("keeps the healthy side visible when only the other side fails to load more", () => {
|
|
|
|
|
const topFetchNextPage = vi.fn().mockResolvedValue(undefined)
|
|
|
|
|
const bottomFetchNextPage = vi.fn().mockResolvedValue(undefined)
|
|
|
|
|
useSectorRadarRankings.mockImplementation((query: { side: string }) =>
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
data: {
|
|
|
|
|
pageParams: [1],
|
|
|
|
|
pages: [
|
|
|
|
|
query.side === "bottom" ? bottomRankingsResponse : rankingsResponse,
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
fetchNextPage:
|
|
|
|
|
query.side === "bottom" ? bottomFetchNextPage : topFetchNextPage,
|
|
|
|
|
isFetchNextPageError: query.side === "bottom",
|
|
|
|
|
hasNextPage: true,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText("新能源")).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole("alert")).toHaveTextContent("弱榜加载更多失败")
|
|
|
|
|
expect(screen.getByRole("button", { name: "重试弱榜" })).toBeInTheDocument()
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "重试弱榜" }))
|
|
|
|
|
expect(bottomFetchNextPage).toHaveBeenCalledOnce()
|
|
|
|
|
expect(topFetchNextPage).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-29 20:14:42 +08:00
|
|
|
it("shows rank changes without inventing missing history", () => {
|
|
|
|
|
routeSearch = { ...routeSearch, view: "rank_change", rankChangeDays: 5 }
|
2026-09-01 11:16:18 +08:00
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
data: {
|
|
|
|
|
pageParams: [1],
|
|
|
|
|
pages: [
|
|
|
|
|
{
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
view: "rank_change",
|
|
|
|
|
rank_change_days: 5,
|
|
|
|
|
rows: rankingsResponse.rows.map((row, index) => ({
|
|
|
|
|
...row,
|
|
|
|
|
rank_change_days: 5,
|
|
|
|
|
rank_change: index === 0 ? 3 : null,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(
|
2026-09-01 12:14:02 +08:00
|
|
|
screen.getByRole("columnheader", { name: "排名上升" }),
|
2026-08-29 20:14:42 +08:00
|
|
|
).toBeInTheDocument()
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("+3 名")[0]).toBeInTheDocument()
|
|
|
|
|
expect(screen.getAllByText("暂无可比历史")[0]).toBeInTheDocument()
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("warns when a partial attempt has not replaced last-good", () => {
|
|
|
|
|
useSectorRadarDates.mockReturnValue({
|
|
|
|
|
data: {
|
|
|
|
|
...datesResponse,
|
|
|
|
|
current_attempt: {
|
|
|
|
|
...successPublication,
|
|
|
|
|
publication_id: "publication-partial",
|
|
|
|
|
target_trade_date: "2026-08-29",
|
|
|
|
|
status: "partial",
|
|
|
|
|
coverage: 0.8,
|
|
|
|
|
input_hash: null,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
isError: false,
|
|
|
|
|
isFetching: false,
|
|
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchDates,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent("新一期数据不完整")
|
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
|
|
|
"当前仍展示最近有效发布 2026-08-28",
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("keeps the last response visible while React Query refreshes it", () => {
|
2026-09-01 11:16:18 +08:00
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({ isFetching: true }),
|
|
|
|
|
)
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
|
|
|
"当前继续展示上一次已读取的有效结果",
|
|
|
|
|
)
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
2026-08-29 20:14:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("keeps stale data visible when a background refresh fails", () => {
|
2026-09-01 11:16:18 +08:00
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({ isError: true }),
|
|
|
|
|
)
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent("雷达数据刷新失败")
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
|
2026-08-29 20:14:42 +08:00
|
|
|
expect(screen.queryByText("板块资金雷达暂时不可用")).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 12:14:02 +08:00
|
|
|
it("shows a fatal error when one side has no data and the other has no response", () => {
|
|
|
|
|
useSectorRadarRankings.mockImplementation((query: { side: string }) =>
|
|
|
|
|
rankingQueryResult(
|
|
|
|
|
query.side === "top"
|
|
|
|
|
? {
|
|
|
|
|
data: {
|
|
|
|
|
pageParams: [1],
|
|
|
|
|
pages: [
|
|
|
|
|
{
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
side: "top",
|
|
|
|
|
status: "no_data",
|
|
|
|
|
publication: null,
|
|
|
|
|
rows: [],
|
|
|
|
|
total: 0,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: { data: undefined, isError: true },
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render(<SectorRadarPage />)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("板块资金雷达暂时不可用")).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-29 20:14:42 +08:00
|
|
|
it("renders loading, error with retry, and no-data states", () => {
|
|
|
|
|
useSectorRadarDates.mockReturnValue({
|
|
|
|
|
data: undefined,
|
|
|
|
|
isError: false,
|
|
|
|
|
isFetching: true,
|
|
|
|
|
isPending: true,
|
|
|
|
|
refetch: refetchDates,
|
|
|
|
|
})
|
|
|
|
|
useSectorRadarRankings.mockReturnValue({
|
|
|
|
|
data: undefined,
|
2026-09-01 11:16:18 +08:00
|
|
|
fetchNextPage,
|
|
|
|
|
hasNextPage: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isError: false,
|
2026-09-01 11:16:18 +08:00
|
|
|
isFetchNextPageError: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isFetching: true,
|
2026-09-01 11:16:18 +08:00
|
|
|
isFetchingNextPage: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isPending: true,
|
|
|
|
|
refetch: refetchRankings,
|
|
|
|
|
})
|
|
|
|
|
const { rerender } = render(<SectorRadarPage />)
|
|
|
|
|
expect(screen.getByLabelText("正在加载板块资金雷达")).toBeInTheDocument()
|
|
|
|
|
|
|
|
|
|
useSectorRadarDates.mockReturnValue({
|
|
|
|
|
data: undefined,
|
|
|
|
|
isError: true,
|
|
|
|
|
isFetching: false,
|
|
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchDates,
|
|
|
|
|
})
|
|
|
|
|
useSectorRadarRankings.mockReturnValue({
|
|
|
|
|
data: undefined,
|
2026-09-01 11:16:18 +08:00
|
|
|
fetchNextPage,
|
|
|
|
|
hasNextPage: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isError: true,
|
2026-09-01 11:16:18 +08:00
|
|
|
isFetchNextPageError: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isFetching: false,
|
2026-09-01 11:16:18 +08:00
|
|
|
isFetchingNextPage: false,
|
2026-08-29 20:14:42 +08:00
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchRankings,
|
|
|
|
|
})
|
|
|
|
|
rerender(<SectorRadarPage />)
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "重试" }))
|
|
|
|
|
expect(refetchDates).toHaveBeenCalledOnce()
|
2026-09-01 12:14:02 +08:00
|
|
|
expect(refetchRankings).toHaveBeenCalledTimes(2)
|
2026-08-29 20:14:42 +08:00
|
|
|
|
|
|
|
|
useSectorRadarDates.mockReturnValue({
|
|
|
|
|
data: {
|
|
|
|
|
status: "no_data",
|
|
|
|
|
available_dates: [],
|
|
|
|
|
current_attempt: null,
|
|
|
|
|
last_good: null,
|
|
|
|
|
},
|
|
|
|
|
isError: false,
|
|
|
|
|
isFetching: false,
|
|
|
|
|
isPending: false,
|
|
|
|
|
refetch: refetchDates,
|
|
|
|
|
})
|
2026-09-01 11:16:18 +08:00
|
|
|
useSectorRadarRankings.mockReturnValue(
|
|
|
|
|
rankingQueryResult({
|
|
|
|
|
data: {
|
|
|
|
|
pageParams: [1],
|
|
|
|
|
pages: [
|
|
|
|
|
{
|
|
|
|
|
...rankingsResponse,
|
|
|
|
|
status: "no_data",
|
|
|
|
|
publication: null,
|
|
|
|
|
rows: [],
|
|
|
|
|
total: 0,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-08-29 20:14:42 +08:00
|
|
|
rerender(<SectorRadarPage />)
|
|
|
|
|
expect(screen.getByText("暂无板块资金发布")).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
})
|