diff --git a/frontend/src/dialogs.tsx b/frontend/src/dialogs.tsx index 0e52997..24828be 100644 --- a/frontend/src/dialogs.tsx +++ b/frontend/src/dialogs.tsx @@ -1,5 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { ChevronLeft, ChevronRight, Download, Save, Trash2, X } from "lucide-react"; +import { + ChevronLeft, + ChevronRight, + Download, + Save, + Trash2, + X, +} from "lucide-react"; import { api } from "./api"; import { labels, @@ -349,7 +356,8 @@ export function EvidenceDialog({ }>(), [side, setSide] = useState<"current" | "baseline">("current"), [page, setPage] = useState(0), - [error, setError] = useState(""); + [error, setError] = useState(""), + [reloadKey, setReloadKey] = useState(0); useEffect(() => { let active = true; setResult(undefined); @@ -364,8 +372,14 @@ export function EvidenceDialog({ return () => { active = false; }; - }, [ds.id, ctx, side, page]); + }, [ds.id, ctx, side, page, reloadKey]); function selectSide(value: "current" | "baseline") { + if (value === side) { + // 重复点击当前期间:仅在不在第一页时回到第一页触发刷新, + // 同期间同页直接返回,避免清空列表后停在“加载中”。 + if (page !== 0) setPage(0); + return; + } setResult(undefined); setPage(0); setSide(value); @@ -401,7 +415,8 @@ export function EvidenceDialog({ {user.role !== "admin" &&

客户原始标识不包含在明细中。

} {error && (

- {error} + {error}{" "} +

)}
@@ -435,6 +450,11 @@ export function EvidenceDialog({ {String(r.gross ?? "")} ))} + {result && result.rows.length === 0 && ( + + 本期范围内没有数据 + + )}
@@ -776,7 +796,10 @@ export function FormulaExportDialog({ ); setDefinitions((items) => [...items.filter((item) => item.id !== saved.id), saved].sort((a, b) => - String(a.definition.name).localeCompare(String(b.definition.name), "zh-CN"), + String(a.definition.name).localeCompare( + String(b.definition.name), + "zh-CN", + ), ), ); setSelectedDefinition(saved.id); diff --git a/tests/e2e/workspace.spec.ts b/tests/e2e/workspace.spec.ts index 20f26af..ddef609 100644 --- a/tests/e2e/workspace.spec.ts +++ b/tests/e2e/workspace.spec.ts @@ -30,7 +30,9 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导 }); await uploaded; const dialog = page.getByRole("dialog"); - await expect(dialog.getByRole("heading", { name: "确认导入字段" })).toBeVisible(); + await expect( + dialog.getByRole("heading", { name: "确认导入字段" }), + ).toBeVisible(); await dialog.getByRole("button", { name: "确认并导入" }).click(); await expect( page.getByRole("heading", { name: "集团经营总览", exact: true }), @@ -85,14 +87,27 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导 await expect(evidenceDialog).toContainText("1 条"); await expect(evidenceDialog).toContainText("2026-08"); await expect(evidenceDialog).toContainText("CSV"); - await evidenceDialog.getByRole("button", { name: "基期", exact: true }).click(); + await evidenceDialog + .getByRole("button", { name: "基期", exact: true }) + .click(); await expect(evidenceDialog).toContainText("2025-08"); await evidenceDialog.getByLabel("关闭", { exact: true }).click(); const evidenceOffsets: number[] = []; + let evidenceFailOnce = false; + let evidenceEmpty = false; await page.route("**/api/datasets/*/evidence", async (route) => { const request = route.request().postDataJSON() as { offset?: number }; const offset = request.offset || 0; + if (evidenceFailOnce) { + evidenceFailOnce = false; + await route.fulfill({ status: 500, json: { error: "服务暂不可用" } }); + return; + } evidenceOffsets.push(offset); + if (evidenceEmpty) { + await route.fulfill({ json: { total: 0, sheet: "CSV", rows: [] } }); + return; + } const count = offset === 0 ? 50 : 1; await route.fulfill({ json: { @@ -111,10 +126,51 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导 }); await page.getByLabel("查看医生A证据", { exact: true }).click(); await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条"); - await evidenceDialog.getByRole("button", { name: "下一页", exact: true }).click(); + await evidenceDialog + .getByRole("button", { name: "下一页", exact: true }) + .click(); await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条"); await expect(evidenceDialog).toContainText("2026-09"); expect(evidenceOffsets).toEqual([0, 50]); + // F03 回归:第二页时重复点击当前期间应回到第一页并重新加载 + await evidenceDialog + .getByRole("button", { name: "本期", exact: true }) + .click(); + await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条"); + expect(evidenceOffsets).toEqual([0, 50, 0]); + // F03 回归:第一页重复点击当前期间不清空列表、不重新请求、不停在“加载中” + await evidenceDialog + .getByRole("button", { name: "本期", exact: true }) + .click(); + await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条"); + await expect(evidenceDialog).not.toContainText("加载中"); + expect(evidenceOffsets).toEqual([0, 50, 0]); + // F03 回归:快速本期/基期切换最终停留在当前期间的数据 + await evidenceDialog + .getByRole("button", { name: "基期", exact: true }) + .click(); + await evidenceDialog + .getByRole("button", { name: "本期", exact: true }) + .click(); + await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条"); + await expect(evidenceDialog).not.toContainText("加载中"); + // F03 回归:请求失败显示错误并可重试恢复 + evidenceFailOnce = true; + await evidenceDialog + .getByRole("button", { name: "下一页", exact: true }) + .click(); + await expect(evidenceDialog).toContainText("服务暂不可用"); + await expect(evidenceDialog).toContainText("加载失败"); + await evidenceDialog.getByRole("button", { name: "重试" }).click(); + await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条"); + // F03 回归:空结果显示空态而不是过期数据或加载中 + evidenceEmpty = true; + await evidenceDialog + .getByRole("button", { name: "上一页", exact: true }) + .click(); + await expect(evidenceDialog).toContainText("本期范围内没有数据"); + await expect(evidenceDialog).toContainText("共 0 条"); + await expect(evidenceDialog).not.toContainText("加载中"); await evidenceDialog.getByLabel("关闭", { exact: true }).click(); await page .getByRole("navigation", { name: "分析路径" }) @@ -164,38 +220,53 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导 await page.route("**/formula-design", async (route) => { designCalls++; await route.fulfill({ - json: designCalls === 1 - ? { complete: false, questions: ["缺失值和零分母如何处理?"], column: null } - : { - complete: true, - questions: [], - column: { - name: "收入成本差", - meaning: "实际收入减成本", - scope: "所选分组与期间", - missing: "结果不可用(#N/A);零分母不可用", - expression: { - operator: "subtract", - left: { metric: "revenue" }, - right: { metric: "cost" }, + json: + designCalls === 1 + ? { + complete: false, + questions: ["缺失值和零分母如何处理?"], + column: null, + } + : { + complete: true, + questions: [], + column: { + name: "收入成本差", + meaning: "实际收入减成本", + scope: "所选分组与期间", + missing: "结果不可用(#N/A);零分母不可用", + expression: { + operator: "subtract", + left: { metric: "revenue" }, + right: { metric: "cost" }, + }, }, }, - }, }); }); await page.getByRole("button", { name: "生成公式工作簿" }).click(); const formulaDialog = page.getByRole("dialog", { name: "生成公式工作簿" }); await formulaDialog.getByRole("checkbox").check(); - await formulaDialog.getByLabel("列计算描述").fill("新增收入成本差,缺失或零分母显示不可用"); + await formulaDialog + .getByLabel("列计算描述") + .fill("新增收入成本差,缺失或零分母显示不可用"); await formulaDialog.getByRole("button", { name: "发送描述" }).click(); await expect(formulaDialog).toContainText("缺失值和零分母如何处理?"); - await formulaDialog.getByLabel("列计算描述").fill("缺失项和零分母都按不可用处理"); + await formulaDialog + .getByLabel("列计算描述") + .fill("缺失项和零分母都按不可用处理"); await formulaDialog.getByRole("button", { name: "发送描述" }).click(); - await formulaDialog.getByRole("button", { name: "确认并生成公式工作簿" }).click(); - await expect(formulaDialog.getByRole("status")).toContainText("生成完成", { timeout: 30000 }); + await formulaDialog + .getByRole("button", { name: "确认并生成公式工作簿" }) + .click(); + await expect(formulaDialog.getByRole("status")).toContainText("生成完成", { + timeout: 30000, + }); const formulaDownload = page.waitForEvent("download"); await formulaDialog.getByRole("button", { name: "下载公式工作簿" }).click(); - expect((await formulaDownload).suggestedFilename()).toBe("traceiq-formula-analysis.xlsx"); + expect((await formulaDownload).suggestedFilename()).toBe( + "traceiq-formula-analysis.xlsx", + ); await formulaDialog.getByLabel("关闭").click(); }); test("手机:登录、总览、设置、Copilot 布局无横向溢出", async ({ page }) => {