2026-08-10 13:42:53 +08:00
|
|
|
|
import {
|
|
|
|
|
|
fireEvent,
|
|
|
|
|
|
render,
|
|
|
|
|
|
screen,
|
|
|
|
|
|
waitFor,
|
|
|
|
|
|
within,
|
|
|
|
|
|
} from "@testing-library/react"
|
2026-08-09 09:34:46 +08:00
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
|
|
|
2026-08-31 16:14:16 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
SelectionPatternScore,
|
|
|
|
|
|
SelectionResults,
|
|
|
|
|
|
} from "../api/selection.types"
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
import { SelectionResultsPage } from "./selection-results-page"
|
|
|
|
|
|
|
2026-08-10 10:07:55 +08:00
|
|
|
|
const routerNavigate = vi.hoisted(() => vi.fn())
|
2026-08-09 09:34:46 +08:00
|
|
|
|
const useSelectionResults = vi.fn()
|
2026-09-01 13:37:19 +08:00
|
|
|
|
const useSelectionChart = vi.fn()
|
2026-08-09 09:34:46 +08:00
|
|
|
|
const useSelectionRun = vi.fn()
|
|
|
|
|
|
const useTriggerSelectionRun = vi.fn()
|
|
|
|
|
|
const mutate = vi.fn()
|
|
|
|
|
|
const reset = vi.fn()
|
2026-09-01 19:27:41 +08:00
|
|
|
|
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),
|
|
|
|
|
|
useSelectionChart: (...args: unknown[]) => useSelectionChart(...args),
|
|
|
|
|
|
useSelectionRun: (...args: unknown[]) => useSelectionRun(...args),
|
|
|
|
|
|
useTriggerSelectionRun: () => useTriggerSelectionRun(),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
2026-08-10 10:07:55 +08:00
|
|
|
|
vi.mock("@tanstack/react-router", () => ({
|
|
|
|
|
|
useNavigate: () => routerNavigate,
|
2026-08-31 16:14:16 +08:00
|
|
|
|
useSearch: () => ({
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 5,
|
|
|
|
|
|
search: "",
|
|
|
|
|
|
category: "all",
|
|
|
|
|
|
sort: "code",
|
|
|
|
|
|
}),
|
2026-08-10 10:07:55 +08:00
|
|
|
|
}))
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
|
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",
|
2026-08-10 11:09:23 +08:00
|
|
|
|
page: 1,
|
|
|
|
|
|
page_size: 5,
|
2026-08-09 09:34:46 +08:00
|
|
|
|
run_id: "run-1",
|
|
|
|
|
|
selected_stock_count: 1,
|
|
|
|
|
|
signal_count: 2,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks_total: 1,
|
|
|
|
|
|
stocks: [
|
2026-08-09 09:34:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
close: 10.5,
|
|
|
|
|
|
name: "平安银行",
|
2026-08-31 16:14:16 +08:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
2026-08-28 11:39:05 +08:00
|
|
|
|
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: 13 },
|
|
|
|
|
|
name: "平安银行",
|
|
|
|
|
|
strategy: "zhixing_b1",
|
|
|
|
|
|
target_trade_date: "2026-08-08",
|
|
|
|
|
|
ts_code: "000001.SZ",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-08-09 09:34:46 +08:00
|
|
|
|
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",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
|
describe("SelectionResultsPage", () => {
|
|
|
|
|
|
beforeEach(() => {
|
2026-09-01 13:37:19 +08:00
|
|
|
|
useSelectionChart.mockReturnValue({
|
|
|
|
|
|
data: undefined,
|
|
|
|
|
|
isError: false,
|
|
|
|
|
|
isPending: false,
|
|
|
|
|
|
})
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(selectionQueryResult(selectedResult))
|
|
|
|
|
|
useSelectionRun.mockReturnValue(emptySelectionQuery())
|
2026-08-09 09:34:46 +08:00
|
|
|
|
useTriggerSelectionRun.mockReturnValue({
|
|
|
|
|
|
isError: false,
|
|
|
|
|
|
isPending: false,
|
|
|
|
|
|
mutate,
|
|
|
|
|
|
reset,
|
|
|
|
|
|
})
|
|
|
|
|
|
mutate.mockReset()
|
|
|
|
|
|
reset.mockReset()
|
2026-08-10 10:07:55 +08:00
|
|
|
|
routerNavigate.mockReset()
|
2026-09-01 19:27:41 +08:00
|
|
|
|
fetchNextPage.mockReset()
|
|
|
|
|
|
fetchNextPage.mockResolvedValue(undefined)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it("renders persisted results and keeps multiple categories visible", () => {
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
2026-08-10 10:07:55 +08:00
|
|
|
|
expect(screen.getByText("执行完成")).toBeInTheDocument()
|
2026-08-10 14:50:56 +08:00
|
|
|
|
expect(screen.queryByText("目标股票数")).not.toBeInTheDocument()
|
2026-09-01 14:57:27 +08:00
|
|
|
|
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()
|
2026-09-01 19:27:41 +08:00
|
|
|
|
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()
|
2026-09-01 11:12:08 +08:00
|
|
|
|
|
2026-08-27 13:17:51 +08:00
|
|
|
|
const detailPanel = screen.getByRole("complementary", {
|
|
|
|
|
|
name: "当前股票详情",
|
|
|
|
|
|
})
|
|
|
|
|
|
expect(within(detailPanel).getByText("关键指标")).toBeInTheDocument()
|
2026-08-31 16:14:16 +08:00
|
|
|
|
expect(within(detailPanel).getByText("图形相似度评分")).toBeInTheDocument()
|
2026-09-01 13:37:19 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
within(detailPanel).getByText("暂无可用行情数据。"),
|
|
|
|
|
|
).toBeInTheDocument()
|
2026-08-31 16:14:16 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
within(detailPanel).getByText("最佳案例:华纳药厂"),
|
|
|
|
|
|
).toBeInTheDocument()
|
|
|
|
|
|
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
|
2026-08-28 11:39:05 +08:00
|
|
|
|
expect(within(detailPanel).getAllByText("J 值")).toHaveLength(2)
|
2026-08-27 13:17:51 +08:00
|
|
|
|
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
|
2026-09-01 13:37:19 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
2026-08-10 10:07:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-31 16:14:16 +08:00
|
|
|
|
it.each<{
|
|
|
|
|
|
expected: string
|
|
|
|
|
|
score: SelectionPatternScore | null
|
|
|
|
|
|
}>([
|
|
|
|
|
|
{
|
|
|
|
|
|
expected: "本次运行未执行图形评分。",
|
|
|
|
|
|
score: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-09-01 13:37:19 +08:00
|
|
|
|
expected: "低于匹配阈值",
|
2026-08-31 16:14:16 +08:00
|
|
|
|
score: {
|
2026-09-01 13:37:19 +08:00
|
|
|
|
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,
|
2026-08-31 16:14:16 +08:00
|
|
|
|
status: "below_threshold",
|
|
|
|
|
|
threshold: 60,
|
2026-09-01 13:37:19 +08:00
|
|
|
|
value: 42.5,
|
2026-08-31 16:14:16 +08:00
|
|
|
|
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 }) => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-31 16:14:16 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
stocks: [{ ...selectedResult.stocks[0], score }],
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-31 16:14:16 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
expect(screen.getAllByText(expected).length).toBeGreaterThan(0)
|
2026-09-01 14:57:27 +08:00
|
|
|
|
const record = screen.getByRole("article")
|
|
|
|
|
|
expect(within(record).getByText("原始 B1")).toBeInTheDocument()
|
|
|
|
|
|
expect(within(record).getByText("12")).toBeInTheDocument()
|
2026-09-01 13:37:19 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
2026-08-31 16:14:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-01 13:37:19 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
useSelectionChart.mockReturnValue({
|
|
|
|
|
|
data: undefined,
|
|
|
|
|
|
isError: true,
|
|
|
|
|
|
isPending: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
view.rerender(<SelectionResultsPage />)
|
|
|
|
|
|
expect(screen.getByText("行情图表暂时不可用。")).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-10 13:42:53 +08:00
|
|
|
|
it("opens execution status details in a drawer and restores trigger focus", async () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-10 13:42:53 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
failed_count: 1,
|
|
|
|
|
|
failures: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "招商银行",
|
|
|
|
|
|
reason: "目标日数据读取失败",
|
|
|
|
|
|
status: "data_error",
|
|
|
|
|
|
ts_code: "600036.SH",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
status: "partial_success",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-10 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
const trigger = screen.getByRole("button", {
|
|
|
|
|
|
name: "查看执行状态:部分成功,未完成评估 1 条",
|
|
|
|
|
|
})
|
2026-08-10 14:50:56 +08:00
|
|
|
|
expect(trigger).toHaveAttribute(
|
|
|
|
|
|
"aria-controls",
|
|
|
|
|
|
"selection-execution-status-drawer",
|
|
|
|
|
|
)
|
|
|
|
|
|
expect(trigger).toHaveAttribute("aria-expanded", "false")
|
2026-08-10 13:42:53 +08:00
|
|
|
|
fireEvent.click(trigger)
|
2026-08-10 14:50:56 +08:00
|
|
|
|
expect(trigger).toHaveAttribute("aria-expanded", "true")
|
2026-08-10 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
const dialog = await screen.findByRole("dialog", {
|
|
|
|
|
|
name: "知行 B1 执行状态",
|
|
|
|
|
|
})
|
2026-08-10 14:50:56 +08:00
|
|
|
|
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()
|
2026-08-10 13:42:53 +08:00
|
|
|
|
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()
|
|
|
|
|
|
})
|
2026-08-10 14:50:56 +08:00
|
|
|
|
expect(trigger).toHaveAttribute("aria-expanded", "false")
|
2026-08-10 13:42:53 +08:00
|
|
|
|
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()
|
|
|
|
|
|
})
|
2026-08-10 14:50:56 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
screen.getByRole("button", {
|
|
|
|
|
|
name: "查看执行状态:执行完成,未完成评估 0 条",
|
|
|
|
|
|
}),
|
|
|
|
|
|
).toHaveFocus()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it("keeps execution status accessible when no signals are hit", async () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-10 14:50:56 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
signal_count: 0,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks: [],
|
|
|
|
|
|
stocks_total: 0,
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-10 14:50:56 +08:00
|
|
|
|
|
|
|
|
|
|
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 () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-10 14:50:56 +08:00
|
|
|
|
...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,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks: [],
|
2026-08-10 14:50:56 +08:00
|
|
|
|
status: "failed",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-10 14:50:56 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
fireEvent.click(
|
|
|
|
|
|
screen.getByRole("button", {
|
|
|
|
|
|
name: "查看执行状态:执行失败,未完成评估 2 条",
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
const dialog = await screen.findByRole("dialog", {
|
|
|
|
|
|
name: "知行 B1 执行状态",
|
|
|
|
|
|
})
|
|
|
|
|
|
expect(within(dialog).getByText("执行失败")).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
it("uses shared selects when the category changes", async () => {
|
2026-08-10 14:50:56 +08:00
|
|
|
|
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: "超卖类" })
|
2026-08-28 11:39:05 +08:00
|
|
|
|
const content = oversoldOption.closest('[data-slot="select-content"]')
|
|
|
|
|
|
expect(content).toHaveAttribute("data-align-trigger", "false")
|
|
|
|
|
|
expect(content?.parentElement).toHaveAttribute("data-align", "start")
|
2026-08-10 14:50:56 +08:00
|
|
|
|
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",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
page: 4,
|
2026-08-10 14:50:56 +08:00
|
|
|
|
pageSize: 5,
|
|
|
|
|
|
search: "",
|
|
|
|
|
|
})
|
2026-08-10 13:42:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
it("requests database-backed score sorting", async () => {
|
2026-08-31 16:14:16 +08:00
|
|
|
|
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",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
page: 3,
|
2026-08-31 16:14:16 +08:00
|
|
|
|
pageSize: 5,
|
|
|
|
|
|
search: "",
|
|
|
|
|
|
sort: "score_desc",
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
it("filters signals and updates the detail panel", () => {
|
2026-08-28 11:39:05 +08:00
|
|
|
|
const extraStock = {
|
|
|
|
|
|
...selectedResult.stocks[0],
|
2026-08-10 10:07:55 +08:00
|
|
|
|
name: "招商银行",
|
2026-08-28 11:39:05 +08:00
|
|
|
|
signals: [
|
|
|
|
|
|
{
|
|
|
|
|
|
...selectedResult.stocks[0].signals[0],
|
|
|
|
|
|
category: "zhixing_b1_oversold_volume",
|
|
|
|
|
|
details: { macd: 3.2 },
|
|
|
|
|
|
name: "招商银行",
|
|
|
|
|
|
ts_code: "600036.SH",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-08-10 10:07:55 +08:00
|
|
|
|
ts_code: "600036.SH",
|
|
|
|
|
|
}
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-10 10:07:55 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
signal_count: 3,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks: [...selectedResult.stocks, extraStock],
|
|
|
|
|
|
stocks_total: 2,
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-10 10:07:55 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
2026-08-28 11:39:05 +08:00
|
|
|
|
fireEvent.change(screen.getByLabelText("搜索命中股票"), {
|
2026-08-10 10:07:55 +08:00
|
|
|
|
target: { value: "招商" },
|
|
|
|
|
|
})
|
2026-09-01 14:57:27 +08:00
|
|
|
|
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: " " })
|
2026-09-01 19:27:41 +08:00
|
|
|
|
const detailPanel = screen.getByRole("complementary", {
|
|
|
|
|
|
name: "当前股票详情",
|
2026-09-01 14:57:27 +08:00
|
|
|
|
})
|
2026-09-01 19:27:41 +08:00
|
|
|
|
expect(within(detailPanel).getByText("招商银行")).toBeInTheDocument()
|
|
|
|
|
|
expect(within(detailPanel).getByText("MACD")).toBeInTheDocument()
|
|
|
|
|
|
expect(within(detailPanel).getByText("3.2")).toBeInTheDocument()
|
|
|
|
|
|
expect(
|
|
|
|
|
|
screen.queryByRole("button", { name: "查看详情" }),
|
|
|
|
|
|
).not.toBeInTheDocument()
|
2026-08-10 10:07:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
it("shows the full result list without pagination controls", () => {
|
2026-08-28 11:39:05 +08:00
|
|
|
|
const stocks = Array.from({ length: 6 }, (_, index) => ({
|
|
|
|
|
|
...selectedResult.stocks[0],
|
2026-08-10 10:07:55 +08:00
|
|
|
|
name: `股票${index + 1}`,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
signals: [
|
|
|
|
|
|
{
|
|
|
|
|
|
...selectedResult.stocks[0].signals[0],
|
|
|
|
|
|
name: `股票${index + 1}`,
|
|
|
|
|
|
ts_code: `00000${index + 1}.SZ`,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-08-10 10:07:55 +08:00
|
|
|
|
ts_code: `00000${index + 1}.SZ`,
|
|
|
|
|
|
}))
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-10 11:09:23 +08:00
|
|
|
|
...selectedResult,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
selected_stock_count: stocks.length,
|
|
|
|
|
|
signal_count: stocks.length,
|
|
|
|
|
|
stocks,
|
|
|
|
|
|
stocks_total: stocks.length,
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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 },
|
2026-08-10 10:07:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-10 10:07:55 +08:00
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent("正在加载更多股票")
|
|
|
|
|
|
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)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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", () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-09 09:34:46 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
failed_count: 1,
|
|
|
|
|
|
failures: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "平安银行",
|
|
|
|
|
|
reason: "目标日数据读取失败",
|
|
|
|
|
|
status: "data_error",
|
|
|
|
|
|
ts_code: "000001.SZ",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
selected_stock_count: 0,
|
|
|
|
|
|
signal_count: 0,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks: [],
|
2026-08-09 09:34:46 +08:00
|
|
|
|
status: "failed",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "重试执行" }))
|
|
|
|
|
|
expect(
|
|
|
|
|
|
screen.getByRole("dialog", { name: "确认重新执行策略?" }),
|
|
|
|
|
|
).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-10 14:50:56 +08:00
|
|
|
|
it("renders a no-data state and triggers an initial run after date selection", async () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
2026-08-09 09:34:46 +08:00
|
|
|
|
...selectedResult,
|
|
|
|
|
|
run_id: null,
|
2026-08-28 11:39:05 +08:00
|
|
|
|
stocks: [],
|
2026-08-09 09:34:46 +08:00
|
|
|
|
status: "no_data",
|
2026-08-10 14:50:56 +08:00
|
|
|
|
target_trade_date: "2026-08-08",
|
2026-09-01 19:27:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("暂无策略结果")).toBeInTheDocument()
|
2026-08-10 14:50:56 +08:00
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "目标交易日" }))
|
|
|
|
|
|
const calendar = await screen.findByRole("grid")
|
|
|
|
|
|
const dateButton = within(calendar).getByRole("button", {
|
|
|
|
|
|
name: "2026年8月9日 星期日",
|
2026-08-09 09:34:46 +08:00
|
|
|
|
})
|
2026-08-10 14:50:56 +08:00
|
|
|
|
fireEvent.click(dateButton)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "执行策略" }))
|
|
|
|
|
|
|
|
|
|
|
|
expect(mutate).toHaveBeenCalledWith(
|
|
|
|
|
|
{
|
|
|
|
|
|
request: {
|
|
|
|
|
|
rerun: false,
|
|
|
|
|
|
strategy: "zhixing_b1",
|
2026-08-10 14:50:56 +08:00
|
|
|
|
target_trade_date: "2026-08-09",
|
2026-08-09 09:34:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-10 14:50:56 +08:00
|
|
|
|
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("选择日期")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
|
it("continues polling a persisted running run after page reload", () => {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
useSelectionResults.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
|
|
|
|
|
...selectedResult,
|
|
|
|
|
|
stocks: [],
|
|
|
|
|
|
status: "running",
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
useSelectionRun.mockReturnValue(
|
|
|
|
|
|
selectionQueryResult({
|
|
|
|
|
|
...selectedResult,
|
|
|
|
|
|
stocks: [],
|
|
|
|
|
|
status: "running",
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("策略执行中")).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByRole("button", { name: "执行中" })).toBeDisabled()
|
2026-08-10 11:09:23 +08:00
|
|
|
|
expect(useSelectionRun).toHaveBeenCalledWith("run-1", {
|
2026-09-01 19:27:41 +08:00
|
|
|
|
pageSize: 20,
|
2026-08-31 16:14:16 +08:00
|
|
|
|
sort: "code",
|
2026-08-10 11:09:23 +08:00
|
|
|
|
})
|
2026-08-09 09:34:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it("renders query failures explicitly", () => {
|
|
|
|
|
|
useSelectionResults.mockReturnValue({
|
2026-09-01 19:27:41 +08:00
|
|
|
|
...emptySelectionQuery(),
|
2026-08-09 09:34:46 +08:00
|
|
|
|
isError: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
render(<SelectionResultsPage />)
|
|
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("结果查询失败")).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|