feat(selection): 重构选股结果筛选与状态入口

This commit is contained in:
yuxuanhui
2026-08-10 14:50:56 +08:00
parent 78be0f53d4
commit 8d4f80f2f1
17 changed files with 1123 additions and 156 deletions
@@ -99,6 +99,7 @@ describe("SelectionResultsPage", () => {
render(<SelectionResultsPage />)
expect(screen.getByText("执行完成")).toBeInTheDocument()
expect(screen.queryByText("目标股票数")).not.toBeInTheDocument()
const table = screen.getByRole("table")
expect(table).toBeInTheDocument()
expect(within(table).getByText("原始 B1")).toBeInTheDocument()
@@ -130,11 +131,25 @@ describe("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: "股票名称" }),
@@ -153,6 +168,7 @@ describe("SelectionResultsPage", () => {
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
})
expect(trigger).toHaveAttribute("aria-expanded", "false")
expect(document.activeElement).toBe(trigger)
})
@@ -175,6 +191,111 @@ describe("SelectionResultsPage", () => {
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({
data: {
...selectedResult,
signal_count: 0,
signals: [],
signals_total: 0,
},
isError: false,
isPending: false,
})
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({
data: {
...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,
signals: [],
status: "failed",
},
isError: false,
isPending: false,
})
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 and resets pagination 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: "超卖类" })
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: 1,
pageSize: 5,
search: "",
})
})
it("filters signals, updates the detail panel, and expands mobile details", () => {
@@ -298,14 +419,14 @@ describe("SelectionResultsPage", () => {
).toBeInTheDocument()
})
it("renders a no-data state and triggers an initial run after date selection", () => {
it("renders a no-data state and triggers an initial run after date selection", async () => {
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
run_id: null,
signals: [],
status: "no_data",
target_trade_date: null,
target_trade_date: "2026-08-08",
},
isError: false,
isPending: false,
@@ -314,9 +435,12 @@ describe("SelectionResultsPage", () => {
render(<SelectionResultsPage />)
expect(screen.getByText("暂无策略结果")).toBeInTheDocument()
fireEvent.change(screen.getByLabelText("目标交易日"), {
target: { value: "2026-08-08" },
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(
@@ -324,13 +448,28 @@ describe("SelectionResultsPage", () => {
request: {
rerun: false,
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
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({
data: { ...selectedResult, signals: [], status: "running" },