feat(sector-radar): 完成前端排名工作台
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const requestJson = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock("@/shared/api/request-json", () => ({ requestJson }))
|
||||
|
||||
import { getSectorRadarDates, getSectorRadarRankings } from "./sector-radar.api"
|
||||
|
||||
const publication = {
|
||||
publication_id: "publication-1",
|
||||
target_trade_date: "2026-08-28",
|
||||
status: "success",
|
||||
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 rankingPayload = {
|
||||
status: "success",
|
||||
requested_trade_date: "2026-08-28",
|
||||
sector_type: "concept",
|
||||
view: "rank_change",
|
||||
rank_change_metric: "amount",
|
||||
rank_change_days: 5,
|
||||
side: "top",
|
||||
search: "机器人",
|
||||
publication,
|
||||
definition: {
|
||||
metric_kind: "amount",
|
||||
metric_version: "zhixing_amount_net_bn_v1",
|
||||
label: "主力净流入(知行独立实现)",
|
||||
unit: "CNY_100M",
|
||||
implementation_kind: "independent",
|
||||
disclaimer: "知行独立实现,非 OneChartLab 原站公式。",
|
||||
},
|
||||
page: 2,
|
||||
page_size: 10,
|
||||
total: 1,
|
||||
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: 5,
|
||||
rank_change: 3,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe("sector radar API adapters", () => {
|
||||
beforeEach(() => {
|
||||
requestJson.mockReset()
|
||||
})
|
||||
|
||||
it("forwards AbortSignal and normalizes decimal publication fields", async () => {
|
||||
const signal = new AbortController().signal
|
||||
requestJson.mockResolvedValue({
|
||||
status: "success",
|
||||
available_dates: ["2026-08-28"],
|
||||
current_attempt: publication,
|
||||
last_good: publication,
|
||||
})
|
||||
|
||||
const result = await getSectorRadarDates(signal)
|
||||
|
||||
expect(requestJson).toHaveBeenCalledWith("/api/v1/sector-radar/dates", {
|
||||
signal,
|
||||
})
|
||||
expect(result.last_good?.coverage).toBe(0.99)
|
||||
})
|
||||
|
||||
it("maps URL-backed filters to the ranking HTTP contract", async () => {
|
||||
const signal = new AbortController().signal
|
||||
requestJson.mockResolvedValue(rankingPayload)
|
||||
|
||||
const result = await getSectorRadarRankings(
|
||||
{
|
||||
tradeDate: "2026-08-28",
|
||||
sectorType: "concept",
|
||||
view: "rank_change",
|
||||
rankChangeMetric: "amount",
|
||||
rankChangeDays: 5,
|
||||
side: "top",
|
||||
search: " 机器人 ",
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
},
|
||||
signal,
|
||||
)
|
||||
|
||||
const [input, init] = requestJson.mock.calls[0] as [
|
||||
string,
|
||||
{ signal?: AbortSignal },
|
||||
]
|
||||
const params = new URL(input, "http://localhost").searchParams
|
||||
expect(input).toContain("/api/v1/sector-radar/rankings?")
|
||||
expect(params.get("trade_date")).toBe("2026-08-28")
|
||||
expect(params.get("sector_type")).toBe("concept")
|
||||
expect(params.get("view")).toBe("rank_change")
|
||||
expect(params.get("rank_change_metric")).toBe("amount")
|
||||
expect(params.get("rank_change_days")).toBe("5")
|
||||
expect(params.get("side")).toBe("top")
|
||||
expect(params.get("search")).toBe("机器人")
|
||||
expect(params.get("page")).toBe("2")
|
||||
expect(params.get("page_size")).toBe("10")
|
||||
expect(init).toEqual({ signal })
|
||||
expect(result.rows[0]?.metric_value).toBe(12.5)
|
||||
expect(result.rows[0]?.rank_percentile).toBe(100)
|
||||
})
|
||||
|
||||
it("rejects unknown stable enum values at the feature boundary", async () => {
|
||||
requestJson.mockResolvedValue({ ...rankingPayload, view: "private_score" })
|
||||
|
||||
await expect(
|
||||
getSectorRadarRankings({
|
||||
sectorType: "concept",
|
||||
view: "amount",
|
||||
rankChangeMetric: "amount",
|
||||
rankChangeDays: 1,
|
||||
side: "all",
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
).rejects.toThrow("Invalid sector radar response: rankings.view")
|
||||
})
|
||||
|
||||
it("rejects non-finite decimal strings instead of rendering them", async () => {
|
||||
requestJson.mockResolvedValue({
|
||||
...rankingPayload,
|
||||
rows: [{ ...rankingPayload.rows[0], metric_value: "NaN" }],
|
||||
})
|
||||
|
||||
await expect(
|
||||
getSectorRadarRankings({
|
||||
sectorType: "concept",
|
||||
view: "amount",
|
||||
rankChangeMetric: "amount",
|
||||
rankChangeDays: 1,
|
||||
side: "all",
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
).rejects.toThrow("rankings.rows[0].metric_value must be finite")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user