368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
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()
|
||
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 条",
|
||
})
|
||
fireEvent.click(trigger)
|
||
|
||
const dialog = await screen.findByRole("dialog", {
|
||
name: "知行 B1 执行状态",
|
||
})
|
||
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(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()
|
||
})
|
||
})
|
||
|
||
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", () => {
|
||
useSelectionResults.mockReturnValue({
|
||
data: {
|
||
...selectedResult,
|
||
run_id: null,
|
||
signals: [],
|
||
status: "no_data",
|
||
target_trade_date: null,
|
||
},
|
||
isError: false,
|
||
isPending: false,
|
||
})
|
||
|
||
render(<SelectionResultsPage />)
|
||
|
||
expect(screen.getByText("暂无策略结果")).toBeInTheDocument()
|
||
fireEvent.change(screen.getByLabelText("目标交易日"), {
|
||
target: { value: "2026-08-08" },
|
||
})
|
||
fireEvent.click(screen.getByRole("button", { name: "执行策略" }))
|
||
|
||
expect(mutate).toHaveBeenCalledWith(
|
||
{
|
||
request: {
|
||
rerun: false,
|
||
strategy: "zhixing_b1",
|
||
target_trade_date: "2026-08-08",
|
||
},
|
||
},
|
||
expect.any(Object),
|
||
)
|
||
})
|
||
|
||
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()
|
||
})
|
||
})
|