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 回复")
);
});
});