Files
zhixing-system/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx
T
yuxuanhui 12642f3c2d feat(selection): rework results layout and add sector filter
- move the filter bar out of the list column into a full-width top section;
  list and detail stay side by side below on md+, stacking search > list >
  detail on small screens
- add sector select fed by the run's concept-board aggregates; options show
  per-sector stock counts ordered by count desc, stale selections reset when
  the aggregate list changes
- persist sector in the route search, results/runs query keys, and refresh
  sector aggregates when a selection run finishes
2026-09-05 19:48:41 +08:00

947 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import type {
SelectionPatternScore,
SelectionResults,
} from "../api/selection.types"
import { SelectionResultsPage } from "./selection-results-page"
const routerNavigate = vi.hoisted(() => vi.fn())
const useSelectionResults = vi.fn()
const useSelectionResultSectors = vi.fn()
const useSelectionChart = vi.fn()
const useStockSectorMembership = vi.fn()
const useSelectionRun = vi.fn()
const useTriggerSelectionRun = vi.fn()
const mutate = vi.fn()
const reset = vi.fn()
const fetchNextPage = vi.fn()
vi.mock("@/features/selection/api/selection.query", async () => {
const actual = await vi.importActual<
typeof import("@/features/selection/api/selection.query")
>("@/features/selection/api/selection.query")
return {
...actual,
useSelectionResults: (...args: unknown[]) => useSelectionResults(...args),
useSelectionResultSectors: (...args: unknown[]) =>
useSelectionResultSectors(...args),
useSelectionChart: (...args: unknown[]) => useSelectionChart(...args),
useSelectionRun: (...args: unknown[]) => useSelectionRun(...args),
useTriggerSelectionRun: () => useTriggerSelectionRun(),
}
})
vi.mock("@/features/sector-radar/api/sector-radar.query", () => ({
useStockSectorMembership: (...args: unknown[]) =>
useStockSectorMembership(...args),
}))
vi.mock("@tanstack/react-router", () => ({
useNavigate: () => routerNavigate,
useSearch: () => ({
page: 1,
pageSize: 5,
search: "",
category: "all",
sort: "code",
}),
}))
const selectedResult: SelectionResults = {
created_at: "2026-08-08T09:00:00+08:00",
coverage: 1,
eligible_count: 2,
evaluated_count: 2,
error_message: null,
error_type: null,
failed_count: 0,
failures: [],
finished_at: "2026-08-08T09:02:00+08:00",
market_sync_batch_id: "market-run-1",
page: 1,
page_size: 5,
run_id: "run-1",
selected_stock_count: 1,
signal_count: 2,
stocks_total: 1,
stocks: [
{
close: 10.5,
name: "平安银行",
score: {
breakdown: {
kdj_state: 83,
price_shape: 90.1,
trend_structure: 71.2,
volume_pattern: 88,
},
case: {
breakout_date: "2025-05-12",
id: "case_001",
name: "华纳药厂",
},
reason: null,
status: "matched",
threshold: 60,
value: 86.4,
version: "zhixing_b1_pattern_fastdtw_v1",
},
signals: [
{
category: "zhixing_b1_original_b1",
close: 10.5,
details: {
daily_amplitude: 3.4,
daily_change: 1.2,
j: 12,
rsi: 20,
},
name: "平安银行",
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
ts_code: "000001.SZ",
},
{
category: "zhixing_b1_pullback_white",
close: 10.5,
details: { j: 13 },
name: "平安银行",
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
ts_code: "000001.SZ",
},
],
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
ts_code: "000001.SZ",
},
],
status: "success",
strategy: "zhixing_b1",
target_count: 2,
target_trade_date: "2026-08-08",
}
function emptySelectionQuery() {
return {
data: undefined,
fetchNextPage,
hasNextPage: false,
isError: false,
isFetchNextPageError: false,
isFetchingNextPage: false,
isPending: false,
}
}
function selectionQueryResult(
result: SelectionResults,
overrides: Record<string, unknown> = {},
) {
return {
data: { pageParams: [1], pages: [result] },
fetchNextPage,
hasNextPage: false,
isError: false,
isFetchNextPageError: false,
isFetchingNextPage: false,
isPending: false,
...overrides,
}
}
describe("SelectionResultsPage", () => {
beforeEach(() => {
useSelectionChart.mockReturnValue({
data: undefined,
isError: false,
isPending: false,
})
useStockSectorMembership.mockReturnValue({
data: undefined,
isError: false,
isPending: false,
})
useSelectionResults.mockReturnValue(selectionQueryResult(selectedResult))
useSelectionResultSectors.mockReturnValue({
data: undefined,
isError: false,
isPending: false,
})
useSelectionRun.mockReturnValue(emptySelectionQuery())
useTriggerSelectionRun.mockReturnValue({
isError: false,
isPending: false,
mutate,
reset,
})
mutate.mockReset()
reset.mockReset()
routerNavigate.mockReset()
fetchNextPage.mockReset()
fetchNextPage.mockResolvedValue(undefined)
})
it("renders persisted results and keeps multiple categories visible", () => {
render(<SelectionResultsPage />)
expect(screen.getByText("执行完成")).toBeInTheDocument()
expect(screen.queryByText("目标股票数")).not.toBeInTheDocument()
expect(screen.queryByRole("table")).not.toBeInTheDocument()
const record = screen.getByRole("article")
expect(within(record).getByText("平安银行")).toBeInTheDocument()
expect(within(record).getByText("000001.SZ")).toBeInTheDocument()
expect(within(record).getByText("原始 B1")).toBeInTheDocument()
expect(within(record).getByText("回踩白线 B")).toBeInTheDocument()
expect(within(record).getByText("86.40 分")).toBeInTheDocument()
expect(within(record).getByText("12")).toBeInTheDocument()
expect(within(record).queryByText("13")).not.toBeInTheDocument()
expect(within(record).getByText("¥ 10.50")).toBeInTheDocument()
expect(screen.getByRole("button", { name: /平安银行/ })).toBeInTheDocument()
expect(
screen.queryByRole("button", { name: "查看详情" }),
).not.toBeInTheDocument()
expect(screen.queryByText(/共 \d+ 条/)).not.toBeInTheDocument()
const rerunButton = screen.getByRole("button", { name: "重新执行" })
const statusTrigger = screen.getByRole("button", {
name: "查看执行状态:执行完成,未完成评估 0 条",
})
expect(
rerunButton.compareDocumentPosition(statusTrigger) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
expect(within(detailPanel).queryByText("关键指标")).not.toBeInTheDocument()
expect(within(detailPanel).getByText("图形相似度评分")).toBeInTheDocument()
expect(
within(detailPanel).getByText("暂无可用行情数据。"),
).toBeInTheDocument()
expect(
within(detailPanel).getByText("最接近案例:华纳药厂"),
).toBeInTheDocument()
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
expect(within(detailPanel).getByText("趋势")).toBeInTheDocument()
expect(within(detailPanel).getByText("量能")).toBeInTheDocument()
expect(within(detailPanel).queryByText("已匹配")).not.toBeInTheDocument()
expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
expect(within(detailPanel).getByText("日涨跌幅")).toBeInTheDocument()
expect(within(detailPanel).getByText("1.2%")).toBeInTheDocument()
expect(within(detailPanel).getByText("日振幅")).toBeInTheDocument()
expect(within(detailPanel).getByText("3.4%")).toBeInTheDocument()
expect(within(detailPanel).getByText("20")).toBeInTheDocument()
const caseImages = within(detailPanel).getAllByRole("img", {
name: "华纳药厂 B1 参考案例图",
})
expect(caseImages).toHaveLength(1)
expect(caseImages[0]).toHaveAttribute(
"src",
"/patterns/b1-cases/case_001_688799_华纳药厂_2025-05-12.jpg",
)
})
it.each<{
expected: string
score: SelectionPatternScore | null
}>([
{
expected: "本次运行未执行图形评分。",
score: null,
},
{
expected: "低于阈值",
score: {
breakdown: {
kdj_state: 42,
price_shape: 44,
trend_structure: 40,
volume_pattern: 43,
},
case: {
breakout_date: "2025-07-23",
id: "case_004",
name: "方正科技",
},
reason: null,
status: "below_threshold",
threshold: 60,
value: 42.5,
version: "zhixing_b1_pattern_fastdtw_v1",
},
},
{
expected: "评分暂不可用",
score: {
breakdown: null,
case: null,
reason: "FastDTW unavailable",
status: "failed",
threshold: null,
value: null,
version: null,
},
},
])(
"keeps selection signals visible when score state changes",
({ expected, score }) => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
stocks: [{ ...selectedResult.stocks[0], score }],
}),
)
render(<SelectionResultsPage />)
expect(screen.getAllByText(expected).length).toBeGreaterThan(0)
const record = screen.getByRole("article")
expect(within(record).getByText("原始 B1")).toBeInTheDocument()
expect(within(record).getByText("12")).toBeInTheDocument()
if (score?.status === "below_threshold") {
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
const images = within(detailPanel).getAllByRole("img", {
name: "方正科技 B1 参考案例图",
})
expect(images).toHaveLength(1)
expect(images[0]).toHaveAttribute(
"src",
"/patterns/b1-cases/case_004_600601_方正科技_2025-07-23.jpg",
)
expect(within(detailPanel).getAllByText("42.50 分")).toHaveLength(2)
} else {
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
expect(within(detailPanel).queryByRole("img")).not.toBeInTheDocument()
}
},
)
it("shows chart loading and error states for the selected stock", () => {
useSelectionChart.mockReturnValue({
data: undefined,
isError: false,
isPending: true,
})
const view = render(<SelectionResultsPage />)
expect(screen.getByText("正在加载行情图表…")).toBeInTheDocument()
expect(useSelectionChart).toHaveBeenLastCalledWith(
"000001.SZ",
"2026-08-08",
"zhixing_b1",
)
useSelectionChart.mockReturnValue({
data: undefined,
isError: true,
isPending: false,
})
view.rerender(<SelectionResultsPage />)
expect(screen.getByText("行情图表暂时不可用。")).toBeInTheDocument()
})
it("opens execution status details in a drawer and restores trigger focus", async () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
failed_count: 1,
failures: [
{
name: "招商银行",
reason: "目标日数据读取失败",
status: "data_error",
ts_code: "600036.SH",
},
],
status: "partial_success",
}),
)
render(<SelectionResultsPage />)
const trigger = screen.getByRole("button", {
name: "查看执行状态:部分成功,未完成评估 1 条",
})
expect(trigger).toHaveAttribute(
"aria-controls",
"selection-execution-status-drawer",
)
expect(trigger).toHaveAttribute("aria-expanded", "false")
fireEvent.click(trigger)
expect(trigger).toHaveAttribute("aria-expanded", "true")
const dialog = await screen.findByRole("dialog", {
name: "知行 B1 执行状态",
})
const summary = within(dialog).getByRole("region", { name: "执行摘要" })
expect(within(summary).getByText("目标股票数")).toBeInTheDocument()
expect(within(summary).getByText("实际参与数")).toBeInTheDocument()
expect(within(summary).getByText("命中股票数")).toBeInTheDocument()
expect(within(summary).getByText("信号条数")).toBeInTheDocument()
expect(within(summary).getByText("数据覆盖率")).toBeInTheDocument()
expect(within(summary).getByText("100.00%")).toBeInTheDocument()
expect(within(summary).getByText("部分成功")).toBeInTheDocument()
const table = within(dialog).getByRole("table")
expect(
within(table).getByRole("columnheader", { name: "股票名称" }),
).toBeInTheDocument()
expect(within(table).getByText("招商银行")).toBeInTheDocument()
expect(within(table).getByText("600036.SH")).toBeInTheDocument()
expect(within(table).getByText("数据读取失败")).toBeInTheDocument()
expect(within(table).getByText("目标日数据读取失败")).toBeInTheDocument()
expect(
screen.queryByRole("heading", { name: "未完成评估" }),
).not.toBeInTheDocument()
fireEvent.click(
within(dialog).getByRole("button", { name: "关闭执行状态" }),
)
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
})
expect(trigger).toHaveAttribute("aria-expanded", "false")
expect(document.activeElement).toBe(trigger)
})
it("shows an empty evaluation table when there are no failures", async () => {
render(<SelectionResultsPage />)
fireEvent.click(
screen.getByRole("button", {
name: "查看执行状态:执行完成,未完成评估 0 条",
}),
)
const dialog = await screen.findByRole("dialog", {
name: "知行 B1 执行状态",
})
expect(within(dialog).getByRole("table")).toBeInTheDocument()
expect(within(dialog).getByText("暂无未完成评估")).toBeInTheDocument()
fireEvent.keyDown(dialog, { key: "Escape" })
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
})
expect(
screen.getByRole("button", {
name: "查看执行状态:执行完成,未完成评估 0 条",
}),
).toHaveFocus()
})
it("keeps execution status accessible when no signals are hit", async () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
signal_count: 0,
stocks: [],
stocks_total: 0,
}),
)
render(<SelectionResultsPage />)
const trigger = screen.getByRole("button", {
name: "查看执行状态:执行完成,未完成评估 0 条",
})
fireEvent.click(trigger)
const dialog = await screen.findByRole("dialog", {
name: "知行 B1 执行状态",
})
expect(within(dialog).getByText("信号条数")).toBeInTheDocument()
})
it("keeps execution status accessible for a failed result", async () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
failed_count: 2,
failures: [
{
name: "招商银行",
reason: "目标日数据读取失败",
status: "data_error",
ts_code: "600036.SZ",
},
{
name: "宁德时代",
reason: "指标计算失败",
status: "calculation_error",
ts_code: "300750.SZ",
},
],
signal_count: 0,
stocks: [],
status: "failed",
}),
)
render(<SelectionResultsPage />)
fireEvent.click(
screen.getByRole("button", {
name: "查看执行状态:执行失败,未完成评估 2 条",
}),
)
const dialog = await screen.findByRole("dialog", {
name: "知行 B1 执行状态",
})
expect(within(dialog).getByText("执行失败")).toBeInTheDocument()
})
it("uses shared selects when the category changes", async () => {
render(<SelectionResultsPage />)
expect(screen.getByRole("combobox", { name: "策略" })).toHaveTextContent(
"知行 B1",
)
const categoryTrigger = screen.getByRole("combobox", {
name: "筛选信号分类",
})
expect(categoryTrigger).toHaveTextContent("全部信号")
fireEvent.click(categoryTrigger)
const oversoldOption = await screen.findByRole("option", { name: "超卖类" })
const content = oversoldOption.closest('[data-slot="select-content"]')
expect(content).toHaveAttribute("data-align-trigger", "false")
expect(content?.parentElement).toHaveAttribute("data-align", "start")
fireEvent.pointerDown(oversoldOption, { pointerType: "mouse" })
fireEvent.click(oversoldOption)
expect(routerNavigate).toHaveBeenCalled()
const lastCall =
routerNavigate.mock.calls[routerNavigate.mock.calls.length - 1]
const searchUpdate = lastCall[0].search as (previous: {
category: string
page: number
pageSize: number
search: string
}) => Record<string, unknown>
expect(
searchUpdate({ category: "all", page: 4, pageSize: 5, search: "" }),
).toEqual({
category: "oversold",
page: 4,
pageSize: 5,
search: "",
})
})
it("filters by sector and shows per-sector counts", async () => {
useSelectionResultSectors.mockReturnValue({
data: {
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
run_id: "run-1",
status: "success",
snapshot_trade_date: "2026-08-08",
sector_type: "concept",
sectors: [
{ sector_code: "BK0001.DC", sector_name: "机器人", stock_count: 3 },
{ sector_code: "BK0003.DC", sector_name: "数字经济", stock_count: 2 },
],
},
isError: false,
isPending: false,
})
render(<SelectionResultsPage />)
const sectorTrigger = screen.getByRole("combobox", { name: "筛选板块" })
expect(sectorTrigger).toHaveTextContent("全部板块")
fireEvent.click(sectorTrigger)
const robotOption = await screen.findByRole("option", {
name: /机器人/,
})
expect(robotOption).toHaveTextContent("3")
fireEvent.pointerDown(robotOption, { pointerType: "mouse" })
fireEvent.click(robotOption)
expect(routerNavigate).toHaveBeenCalled()
const lastCall =
routerNavigate.mock.calls[routerNavigate.mock.calls.length - 1]
const searchUpdate = lastCall[0].search as (previous: {
sector?: string
}) => Record<string, unknown>
expect(searchUpdate({})).toEqual({ sector: "BK0001.DC" })
})
it("requests database-backed score sorting", async () => {
render(<SelectionResultsPage />)
const sortTrigger = screen.getByRole("combobox", {
name: "排序命中股票",
})
fireEvent.click(sortTrigger)
const option = await screen.findByRole("option", { name: "评分从高到低" })
fireEvent.pointerDown(option, { pointerType: "mouse" })
fireEvent.click(option)
const lastCall = routerNavigate.mock.calls.at(-1)
const searchUpdate = lastCall?.[0].search as (previous: {
category: string
page: number
pageSize: number
search: string
sort: string
}) => Record<string, unknown>
expect(
searchUpdate({
category: "all",
page: 3,
pageSize: 5,
search: "",
sort: "code",
}),
).toEqual({
category: "all",
page: 3,
pageSize: 5,
search: "",
sort: "score_desc",
})
})
it("filters signals and updates the detail panel", () => {
const extraStock = {
...selectedResult.stocks[0],
name: "招商银行",
signals: [
{
...selectedResult.stocks[0].signals[0],
category: "zhixing_b1_oversold_volume",
details: { macd: 3.2 },
name: "招商银行",
ts_code: "600036.SH",
},
],
ts_code: "600036.SH",
}
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
signal_count: 3,
stocks: [...selectedResult.stocks, extraStock],
stocks_total: 2,
}),
)
render(<SelectionResultsPage />)
fireEvent.change(screen.getByLabelText("搜索命中股票"), {
target: { value: "招商" },
})
const extraRecord = screen
.getAllByRole("article")
.find((article) => within(article).queryByText("招商银行"))
expect(extraRecord).toBeTruthy()
if (!extraRecord) {
throw new Error("expected 招商银行 card to be visible")
}
const selectButton = within(extraRecord).getByRole("button", {
name: /招商银行/,
})
fireEvent.keyDown(selectButton, { key: " " })
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
expect(within(detailPanel).getByText("招商银行")).toBeInTheDocument()
expect(within(detailPanel).queryByText("MACD")).not.toBeInTheDocument()
expect(within(detailPanel).queryByText("3.2")).not.toBeInTheDocument()
expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
expect(
screen.queryByRole("button", { name: "查看详情" }),
).not.toBeInTheDocument()
})
it("shows the full result list without pagination controls", () => {
const stocks = Array.from({ length: 6 }, (_, index) => ({
...selectedResult.stocks[0],
name: `股票${index + 1}`,
signals: [
{
...selectedResult.stocks[0].signals[0],
name: `股票${index + 1}`,
ts_code: `00000${index + 1}.SZ`,
},
],
ts_code: `00000${index + 1}.SZ`,
}))
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
selected_stock_count: stocks.length,
signal_count: stocks.length,
stocks,
stocks_total: stocks.length,
}),
)
render(<SelectionResultsPage />)
expect(screen.getAllByText("股票1").length).toBeGreaterThan(0)
expect(screen.getByText("股票6")).toBeInTheDocument()
expect(screen.queryByText("第 1–5 条,共 6 条")).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: "第 2 页" }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("combobox", { name: "每页数量" }),
).not.toBeInTheDocument()
})
it("loads the next page once when repeated scroll events reach the bottom", () => {
fetchNextPage.mockReturnValue(new Promise(() => undefined))
useSelectionResults.mockReturnValue(
selectionQueryResult(
{ ...selectedResult, stocks_total: 40 },
{ hasNextPage: true },
),
)
render(<SelectionResultsPage />)
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", () => {
useSelectionResults.mockReturnValue(
selectionQueryResult(selectedResult, {
hasNextPage: true,
isFetchingNextPage: true,
}),
)
render(<SelectionResultsPage />)
expect(screen.getByText("正在加载更多股票…")).toBeInTheDocument()
expect(screen.getAllByText("平安银行").length).toBeGreaterThan(0)
})
it("keeps loaded rows and offers retry when loading more fails", () => {
useSelectionResults.mockReturnValue(
selectionQueryResult(selectedResult, {
hasNextPage: true,
isError: true,
isFetchNextPageError: true,
}),
)
render(<SelectionResultsPage />)
expect(screen.getAllByText("平安银行").length).toBeGreaterThan(0)
expect(screen.getByRole("alert")).toHaveTextContent(
"加载更多失败,已加载结果仍保留",
)
fireEvent.click(screen.getByRole("button", { name: "重试" }))
expect(fetchNextPage).toHaveBeenCalledOnce()
})
it("merges stocks from every loaded page", () => {
const nextStock = {
...selectedResult.stocks[0],
name: "招商银行",
ts_code: "600036.SH",
signals: [
{
...selectedResult.stocks[0].signals[0],
name: "招商银行",
ts_code: "600036.SH",
},
],
}
useSelectionResults.mockReturnValue(
selectionQueryResult(selectedResult, {
data: {
pageParams: [1, 2],
pages: [
{ ...selectedResult, stocks_total: 2 },
{
...selectedResult,
page: 2,
stocks: [nextStock],
stocks_total: 2,
},
],
},
}),
)
render(<SelectionResultsPage />)
expect(screen.getAllByText("平安银行").length).toBeGreaterThan(0)
expect(screen.getAllByText("招商银行").length).toBeGreaterThan(0)
})
it("requires confirmation before a rerun and does not trigger on cancel", () => {
render(<SelectionResultsPage />)
fireEvent.click(screen.getByRole("button", { name: "重新执行" }))
const dialog = screen.getByRole("dialog", { name: "确认重新执行策略?" })
expect(within(dialog).getByText(/清空 2026-08-08/)).toBeInTheDocument()
fireEvent.click(within(dialog).getByRole("button", { name: "取消" }))
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
expect(mutate).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole("button", { name: "重新执行" }))
fireEvent.click(
within(
screen.getByRole("dialog", { name: "确认重新执行策略?" }),
).getByRole("button", { name: "确认重新执行" }),
)
expect(mutate).toHaveBeenCalledWith(
{
request: {
rerun: true,
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
},
},
expect.any(Object),
)
})
it("uses the same confirmation flow for a failed retry", () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
failed_count: 1,
failures: [
{
name: "平安银行",
reason: "目标日数据读取失败",
status: "data_error",
ts_code: "000001.SZ",
},
],
selected_stock_count: 0,
signal_count: 0,
stocks: [],
status: "failed",
}),
)
render(<SelectionResultsPage />)
fireEvent.click(screen.getByRole("button", { name: "重试执行" }))
expect(
screen.getByRole("dialog", { name: "确认重新执行策略?" }),
).toBeInTheDocument()
})
it("renders a no-data state and triggers an initial run after date selection", async () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
run_id: null,
stocks: [],
status: "no_data",
target_trade_date: "2026-08-08",
}),
)
render(<SelectionResultsPage />)
expect(screen.getByText("暂无策略结果")).toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "目标交易日" }))
const calendar = await screen.findByRole("grid")
const dateButton = within(calendar).getByRole("button", {
name: "2026年8月9日 星期日",
})
fireEvent.click(dateButton)
fireEvent.click(screen.getByRole("button", { name: "执行策略" }))
expect(mutate).toHaveBeenCalledWith(
{
request: {
rerun: false,
strategy: "zhixing_b1",
target_trade_date: "2026-08-09",
},
},
expect.any(Object),
)
})
it("keeps the target trade date empty after clearing the date picker", async () => {
render(<SelectionResultsPage />)
const dateTrigger = screen.getByRole("button", { name: "目标交易日" })
fireEvent.click(dateTrigger)
const calendar = await screen.findByRole("grid")
fireEvent.click(
within(calendar).getByRole("button", {
name: "2026年8月8日 星期六,已选择",
}),
)
expect(dateTrigger).toHaveTextContent("选择日期")
})
it("continues polling a persisted running run after page reload", () => {
useSelectionResults.mockReturnValue(
selectionQueryResult({
...selectedResult,
stocks: [],
status: "running",
}),
)
useSelectionRun.mockReturnValue(
selectionQueryResult({
...selectedResult,
stocks: [],
status: "running",
}),
)
render(<SelectionResultsPage />)
expect(screen.getByText("策略执行中")).toBeInTheDocument()
expect(screen.getByRole("button", { name: "执行中" })).toBeDisabled()
expect(useSelectionRun).toHaveBeenCalledWith("run-1", {
pageSize: 20,
sort: "code",
})
})
it("renders query failures explicitly", () => {
useSelectionResults.mockReturnValue({
...emptySelectionQuery(),
isError: true,
})
render(<SelectionResultsPage />)
expect(screen.getByText("结果查询失败")).toBeInTheDocument()
})
})