import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { AnswerUiView } from "./answer-ui-view"; import { parseAnswerUiBlock } from "./contract"; vi.mock("echarts-for-react", () => ({ default: () =>
})); function block(type: string, props: Record) { const parsed = parseAnswerUiBlock({ version: "answer-ui@1", block_id: "test", fallback_text: "演练", spec: { root: "node", elements: { node: { type, props, children: [] } } } }); if (!parsed) throw new Error("invalid test fixture"); return parsed; } function taskBlock(includeData = true) { const children = ["metric", ...(includeData ? ["table"] : []), "evidence", "form"]; const fixture = { version: "answer-ui@1", block_id: "task", fallback_text: "演练分析", spec: { root: "root", elements: { root: { type: "Stack", props: { columns: "one", gap: "normal" }, children }, metric: { type: "Metric", props: { label: "当前压力", value: "0.2130", unit: "MPa", source: "监测快照" }, children: [] }, ...(includeData ? { table: { type: "Table", props: { title: "入口记录", columns: ["对象", "压力(MPa)"], rows: [["北区入口", "0.2130"], ["南区入口", "0.2870"]] }, children: [] } } : {}), evidence: { type: "Evidence", props: { title: "读数依据", summary: "仅用于演练", source: "测试查询", kind: "demo", evidence_id: "DEMO-26", observed_at: null }, children: [] }, form: { type: "FollowUpForm", props: { title: "核验资料", description: "主动补充", fields: [{ name: "record", label: "现场记录", kind: "text", required: true }] }, children: [] } } } }; const parsed = parseAnswerUiBlock(fixture); if (!parsed) throw new Error("invalid task fixture"); return parsed; } describe("local interactive answer renderer", () => { it("shows navigation for long answers while keeping short metric and evidence replies quiet", () => { const fixture = taskBlock(false); fixture.spec.elements.root.children = ["metric", "evidence"]; delete fixture.spec.elements.form; const { rerender } = render(); expect(screen.getByRole("group", { name: "当前压力" })).toBeVisible(); expect(screen.queryByRole("navigation", { name: "回答内容导航" })).not.toBeInTheDocument(); const longer = structuredClone(fixture); longer.spec.elements.evidence.props.summary = "待核对的监测资料。".repeat(150); rerender(); expect(screen.getByRole("navigation", { name: "回答内容导航" })).toBeVisible(); }); it("uses a single form and evidence card's own headings without fixed wrapper instructions", () => { render(); expect(screen.getByLabelText("现场记录(必填)")).toBeVisible(); expect(screen.getByRole("button", { name: /读数依据/ })).toBeVisible(); expect(screen.queryByRole("button", { name: "收起补充资料" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "收起核查依据" })).not.toBeInTheDocument(); expect(screen.queryByText("直接探索已加载数据")).not.toBeInTheDocument(); expect(screen.queryByText("区分记录、推断与用户资料")).not.toBeInTheDocument(); }); it("adapts response and report composition without replacing a reader's form draft", async () => { const user = userEvent.setup(); const fixture = taskBlock(); fixture.spec.elements.root.props = { columns: "one", gap: "normal", intent: "response", density: "compact" }; const { rerender, container } = render(); const editor = screen.getByLabelText("现场记录(必填)"); await user.type(editor, "保留的现场草稿"); expect(container.querySelector("[data-answer-task-layout]")).toBeNull(); const report = { ...fixture, spec: { ...fixture.spec, elements: { ...fixture.spec.elements, root: { ...fixture.spec.elements.root, props: { columns: "one", gap: "normal", intent: "report", density: "comfortable" } } } } }; rerender(); expect(container.querySelector("[data-answer-task-layout]")).not.toBeNull(); expect(screen.getByLabelText("现场记录(必填)")).toBe(editor); expect(editor).toHaveValue("保留的现场草稿"); }); it("renders evidence-bound result states without treating historical results as live alerts", () => { const fixture = (status: string) => block("Result", { title: "管道定位结果", summary: "已定位 3 根管道;高亮尚未确认。", status, source: "地图定位回执" }); const { rerender } = render(); const result = screen.getByRole("group", { name: "管道定位结果" }); expect(result).toHaveTextContent("部分完成"); expect(result).toHaveTextContent("依据:地图定位回执"); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); for (const [status, label] of [["success", "成功"], ["unconfirmed", "未确认"], ["error", "失败"]]) { rerender(); expect(result).toHaveTextContent(label); expect(result).toHaveAttribute("data-result-status", status); } }); it("uses the supplied note title without changing the warning meaning", () => { render(); expect(screen.getByRole("note")).toHaveTextContent("能力限制"); expect(screen.getByRole("note")).not.toHaveTextContent("注意事项"); }); it("shows data and evidence without row selection, quoting or analysis controls", async () => { const user = userEvent.setup(); const submit = vi.fn(); render(); expect(screen.getByRole("group", { name: "当前压力" })).toHaveTextContent("0.2130MPa"); expect(screen.getByRole("table")).toHaveTextContent("北区入口"); expect(screen.getAllByRole("columnheader")).toHaveLength(2); await user.click(screen.getByRole("button", { name: /读数依据.*测试查询/ })); await waitFor(() => expect(screen.getByText("仅用于演练")).toBeVisible()); expect(screen.getByText("DEMO-26")).toBeVisible(); expect(screen.queryByRole("checkbox")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /加入分析|针对.*提问|取消选行/ })).not.toBeInTheDocument(); expect(screen.queryByRole("region", { name: "分析内容篮" })).not.toBeInTheDocument(); expect(submit).not.toHaveBeenCalled(); }); it("keeps the same form when more data arrives and support sections collapse", async () => { const user = userEvent.setup(); const { rerender } = render(); const editor = screen.getByLabelText("现场记录(必填)"); await user.type(editor, "未发送的现场草稿"); const grouped = taskBlock(); grouped.spec.elements.root.children = [...(grouped.spec.elements.root.children ?? []), "extra-form"]; grouped.spec.elements["extra-form"] = { ...grouped.spec.elements.form, props: { title: "其他资料", description: "可选补充", fields: [{ name: "extra", label: "其他记录", kind: "text", required: false }] } }; rerender(); expect(screen.getByLabelText("现场记录(必填)")).toBe(editor); expect(editor).toHaveValue("未发送的现场草稿"); await user.click(screen.getByRole("button", { name: "收起补充资料" })); expect(editor.closest("[data-answer-reveal]")).toHaveAttribute("inert"); await waitFor(() => expect(editor).not.toBeVisible()); await user.click(screen.getByRole("button", { name: "展开补充资料" })); await waitFor(() => expect(editor).toBeVisible()); expect(editor).toHaveValue("未发送的现场草稿"); await user.click(screen.getByRole("button", { name: "收起补充资料" })); const updated = taskBlock(); updated.spec.elements.table.props.rows = [["新增入口", "0.24"]]; rerender(); expect(screen.getByLabelText("现场记录(必填)")).toBe(editor); await waitFor(() => expect(editor).toBeVisible()); expect(editor).toHaveValue("未发送的现场草稿"); }); it("preserves long original cell values without requiring a selection or quote action", () => { const fixture = taskBlock(); fixture.spec.elements.table.props.rows = Array.from({ length: 12 }, (_, index) => [String(index), "长".repeat(800)]); render(); expect(screen.getAllByText("长".repeat(800))).toHaveLength(12); expect(screen.queryByRole("checkbox")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /加入分析|针对.*提问/ })).not.toBeInTheDocument(); }); it("expresses supplied context and cautions as readable notes without announcing historical alerts", () => { const { rerender } = render(); expect(screen.getByRole("note")).toHaveTextContent("注意事项"); expect(screen.getByRole("note")).toHaveTextContent("压力记录尚未经过现场核验。"); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect(screen.queryByRole("navigation")).not.toBeInTheDocument(); rerender(); expect(screen.getByRole("note")).toHaveTextContent("补充说明"); rerender(); expect(screen.queryByRole("note")).not.toBeInTheDocument(); expect(screen.getByText("普通回答")).toBeInTheDocument(); }); it("retains a metric's exact value, unit and source without action buttons", () => { render(); const metric = screen.getByRole("group", { name: "当前压力" }); expect(metric).toHaveTextContent("0.3000MPa"); expect(metric).toHaveTextContent("来源:已加载记录"); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); it("renders through json-render and lets readers inspect exact chart data with keyboard tabs", async () => { const user = userEvent.setup(); render( ); await user.click(screen.getByRole("tab", { name: /^数据$/ })); expect(screen.getByRole("table")).toHaveTextContent("0.3"); expect(screen.getByRole("table")).toHaveTextContent("09:00"); expect(screen.getByRole("tab", { name: /^数据$/ })).toHaveAttribute("aria-selected", "true"); await user.keyboard("{ArrowLeft}"); expect(screen.getByRole("tab", { name: /^柱状$/ })).toHaveFocus(); expect(screen.getByRole("tab", { name: /^柱状$/ })).toHaveAttribute("aria-selected", "true"); expect(screen.queryByRole("table")).not.toBeInTheDocument(); }); it("runs map location only after a user click and reports missing IDs", async () => { const locate = vi .fn() .mockResolvedValue({ located_ids: ["DEMO-P001"], missing_ids: ["DEMO-P002"] }); render( ); expect(locate).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "定位演练监测点" })); await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent("另有 1 个未找到")); expect(locate).toHaveBeenCalledWith({ layer: "scada", feature_ids: ["DEMO-P001", "DEMO-P002"] }); }); it("filters and sorts loaded rows without selecting or sending content", async () => { const user = userEvent.setup(); const submit = vi.fn(); render(); await user.type(screen.getByRole("textbox", { name: "搜索监测记录" }), "南站"); expect(screen.getByText("2 / 3 行")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "按压力排序" })); const rows = screen.getAllByRole("row"); expect(rows[1]).toHaveTextContent("南站二号"); expect(rows[1]).toHaveTextContent("0.26"); expect(screen.queryByRole("checkbox")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /加入分析|针对.*提问/ })).not.toBeInTheDocument(); expect(submit).not.toHaveBeenCalled(); }); it("keeps form values after a failed submission", async () => { const user = userEvent.setup(); render( { throw new Error("暂未发送"); }} />); await user.type(screen.getByLabelText("记录(必填)"), "北站现场记录"); await user.click(screen.getByRole("button", { name: "发送补充资料" })); expect(await screen.findByRole("status")).toHaveTextContent("暂未发送"); expect(screen.getByLabelText("记录(必填)")).toHaveValue("北站现场记录"); }); it("starts blank, requires input and sends one user message without claiming evidence is saved", async () => { let complete!: () => void; const submit = vi.fn( () => new Promise((resolve) => { complete = resolve; }) ); render( ); expect(screen.getByLabelText("现场位置(必填)")).toHaveValue(""); fireEvent.submit(screen.getByRole("form", { name: "补充现场资料" })); expect(submit).not.toHaveBeenCalled(); expect(screen.getByRole("status")).toHaveTextContent("请填写必填资料"); fireEvent.change(screen.getByLabelText("现场位置(必填)"), { target: { value: "用户指定位置" } }); fireEvent.submit(screen.getByRole("form", { name: "补充现场资料" })); expect(submit).toHaveBeenCalledTimes(1); expect(submit.mock.calls[0]).toEqual([expect.stringContaining("现场位置:用户指定位置")]); expect(screen.getByRole("button", { name: "正在发送…" })).toBeDisabled(); complete(); await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent("资料已发送,请查看 Agent 回复") ); }); });