Files
zhixing-system/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx
T

507 lines
15 KiB
TypeScript
Raw Normal View History

import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import type { SelectionResults } from "../api/selection.types"
import { SelectionResultsPage } from "./selection-results-page"
const routerNavigate = vi.hoisted(() => vi.fn())
const useSelectionResults = vi.fn()
const useSelectionRun = vi.fn()
const useTriggerSelectionRun = vi.fn()
const mutate = vi.fn()
const reset = vi.fn()
vi.mock("@/features/selection/api/selection.query", () => ({
useSelectionResults: (...args: unknown[]) => useSelectionResults(...args),
useSelectionRun: (...args: unknown[]) => useSelectionRun(...args),
useTriggerSelectionRun: () => useTriggerSelectionRun(),
}))
vi.mock("@tanstack/react-router", () => ({
useNavigate: () => routerNavigate,
useSearch: () => ({ page: 1, pageSize: 5, search: "", category: "all" }),
}))
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,
signals_total: 2,
signals: [
{
category: "zhixing_b1_original_b1",
close: 10.5,
details: { 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: 12 },
name: "平安银行",
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",
}
describe("SelectionResultsPage", () => {
beforeEach(() => {
useSelectionResults.mockReturnValue({
data: selectedResult,
isError: false,
isPending: false,
})
useSelectionRun.mockReturnValue({
data: undefined,
isError: false,
isPending: false,
})
useTriggerSelectionRun.mockReturnValue({
isError: false,
isPending: false,
mutate,
reset,
})
mutate.mockReset()
reset.mockReset()
routerNavigate.mockReset()
})
it("renders persisted results and keeps multiple categories visible", () => {
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()
expect(within(table).getByText("回踩白线 B")).toBeInTheDocument()
expect(within(table).getAllByText("平安银行")).toHaveLength(2)
})
it("opens execution status details in a drawer and restores trigger focus", async () => {
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
failed_count: 1,
failures: [
{
name: "招商银行",
reason: "目标日数据读取失败",
status: "data_error",
ts_code: "600036.SH",
},
],
status: "partial_success",
},
isError: false,
isPending: false,
})
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({
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", () => {
const extraSignal = {
...selectedResult.signals[0],
category: "zhixing_b1_oversold_volume",
details: { macd: 3.2 },
name: "招商银行",
ts_code: "600036.SH",
}
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
signal_count: 3,
signals: [...selectedResult.signals, extraSignal],
},
isError: false,
isPending: false,
})
render(<SelectionResultsPage />)
fireEvent.change(screen.getByLabelText("搜索命中信号"), {
target: { value: "招商" },
})
const table = screen.getByRole("table")
expect(within(table).getByText("招商银行")).toBeInTheDocument()
fireEvent.click(within(table).getByText("招商银行"))
expect(
within(
screen.getByRole("complementary", { name: "当前股票详情" }),
).getByText("招商银行"),
).toBeInTheDocument()
const expandButton = screen.getAllByRole("button", { name: "查看详情" })[0]
fireEvent.click(expandButton)
expect(expandButton).toHaveAttribute("aria-expanded", "true")
expect(screen.getAllByText("macd: 3.2").length).toBeGreaterThan(1)
})
it("navigates to another page when the result list is paginated", () => {
const signals = Array.from({ length: 6 }, (_, index) => ({
...selectedResult.signals[0],
name: `股票${index + 1}`,
ts_code: `00000${index + 1}.SZ`,
}))
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
signal_count: signals.length,
signals,
signals_total: signals.length,
},
isError: false,
isPending: false,
})
render(<SelectionResultsPage />)
expect(screen.getByText("第 1–5 条,共 6 条")).toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "第 2 页" }))
expect(routerNavigate).toHaveBeenCalled()
})
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({
data: {
...selectedResult,
failed_count: 1,
failures: [
{
name: "平安银行",
reason: "目标日数据读取失败",
status: "data_error",
ts_code: "000001.SZ",
},
],
selected_stock_count: 0,
signal_count: 0,
signals: [],
status: "failed",
},
isError: false,
isPending: false,
})
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({
data: {
...selectedResult,
run_id: null,
signals: [],
status: "no_data",
target_trade_date: "2026-08-08",
},
isError: false,
isPending: false,
})
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({
data: { ...selectedResult, signals: [], status: "running" },
isError: false,
isPending: false,
})
useSelectionRun.mockReturnValue({
data: { ...selectedResult, signals: [], status: "running" },
isError: false,
isPending: false,
})
render(<SelectionResultsPage />)
expect(screen.getByText("策略执行中")).toBeInTheDocument()
expect(screen.getByRole("button", { name: "执行中" })).toBeDisabled()
expect(useSelectionRun).toHaveBeenCalledWith("run-1", {
page: 1,
pageSize: 5,
})
})
it("renders query failures explicitly", () => {
useSelectionResults.mockReturnValue({
data: undefined,
isError: true,
isPending: false,
})
render(<SelectionResultsPage />)
expect(screen.getByText("结果查询失败")).toBeInTheDocument()
})
})