import { fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DataFlowRenderer } from "@/components/inspector/DataFlowRenderer"; import { Inspector } from "@/components/inspector/Inspector"; import { getCardData } from "@/lib/api"; import { finalResultItem, storyItem } from "@/lib/layout"; import type { CardBusinessData } from "@/lib/types"; import { makeStory, makeView } from "@/tests/fixtures"; function makeCardData(overrides: Partial = {}): CardBusinessData { return { cardKind: "data-decision", relationKind: "business-record", completeness: "partial", businessInputs: [{ id: "input:cases", label: "候选案例", value: "案例 A、案例 B", source: { kind: "database", label: "取数结果", ref: "retrieval-agent:3786", fieldPath: "output.cases" }, relation: "available-upstream", completeness: "complete", }], runtime: { summary: "实现 Agent 根据可用材料形成取舍记录。", units: [{ id: "event:3786", label: "解构 Case Agent", status: "completed", durationMs: 1250, summary: "返回两个候选案例。", input: "查找相关案例", output: { count: 2 }, eventRef: "event:3786", completeness: "complete" }], calculation: "按同一分支和时间顺序聚合,不使用文本相似度补链。", }, businessOutputs: [{ id: "output:decision", label: "明确取舍", value: "采用案例 A,舍弃案例 B。", source: { kind: "database", label: "数据取舍记录", ref: "data-decision:115" }, relation: "persisted-output", completeness: "complete", }], displayUse: { businessModules: [{ id: "decision", title: "明确取舍", sourceIds: ["output:decision"] }], cardFields: [{ key: "headline", label: "取舍结论", sourceIds: ["output:decision"], transform: "直接使用结构化结论" }], unusedSourceIds: ["input:cases"], }, notices: [{ level: "warning", message: "上游材料存在,但运行记录没有证明它被本次决策采用。" }], ...overrides, }; } describe("card data flow", () => { afterEach(() => vi.unstubAllGlobals()); it("keeps the old Inspector focused on business, changes and technical detail", () => { const item = storyItem(makeStory("candidate", "candidate-output", "result", { hasChanges: true })); const detail = { businessSections: [], changes: { sections: [], exactness: "exact" }, technical: {} }; render(); expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["业务详情", "产出与改动", "技术详情"]); }); it("renders the vertical lineage, explicit uncertainty and display mappings", () => { render(); expect(screen.getByRole("heading", { name: "数据关系与完整性" })).toBeInTheDocument(); expect(screen.getByText("业务记录")).toBeInTheDocument(); expect(screen.getByText("部分记录")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "业务输入" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "运行过程 / I/O" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "业务输出" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "页面如何使用这些数据" })).toBeInTheDocument(); expect(screen.getByText("可确认是上游信息,但无法确认是否被本次决策采用")).toBeInTheDocument(); expect(screen.getAllByText("数据库")).toHaveLength(2); expect(screen.getByText("直接使用结构化结论")).toBeInTheDocument(); expect(screen.getAllByText("F2")).toHaveLength(3); }); it("uses keyboard-friendly disclosures for runtime units and long values", () => { const longText = "完整业务正文。".repeat(150); render(); const runtimeSummary = screen.getByText("解构 Case Agent").closest("summary"); expect(runtimeSummary).toHaveAttribute("aria-expanded", "false"); fireEvent.click(runtimeSummary!); expect(runtimeSummary).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText("输入")).toBeInTheDocument(); const longSummary = screen.getByText("展开完整内容").closest("summary"); expect(longSummary).toHaveAttribute("aria-expanded", "false"); expect(screen.queryByText(longText)).not.toBeInTheDocument(); fireEvent.click(longSummary!); expect(screen.getByText("收起完整内容").closest("summary")).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText(longText)).toBeInTheDocument(); }); it("shows dedicated loading, error and missing-reference states", () => { const { rerender } = render(); expect(screen.getByLabelText("正在加载数据流")).toBeInTheDocument(); rerender(); expect(screen.getByText("数据流暂时不可用")).toBeInTheDocument(); expect(screen.getByText("API 503")).toBeInTheDocument(); rerender(); expect(screen.getByText("没有稳定的数据定位")).toBeInTheDocument(); }); it("requests card data with the run and encoded detail reference", async () => { const payload = makeCardData(); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => payload }); vi.stubGlobal("fetch", fetchMock); await expect(getCardData("443", "run:final-result")).resolves.toEqual(payload); expect(fetchMock).toHaveBeenCalledWith( "http://127.0.0.1:8788/api/script-builds/443/card-data/run%3Afinal-result", expect.objectContaining({ cache: "no-store" }), ); }); it("uses an independent final-result detail while preserving the artifact action", () => { const view = makeView(); const item = finalResultItem(view); const onArtifact = vi.fn(); expect(item.detailRef).toBe("run:final-result"); render(); fireEvent.click(screen.getByRole("button", { name: "查看当前主脚本" })); expect(onArtifact).toHaveBeenCalledOnce(); expect(view.finalResult.artifact.snapshotRef).toBe("artifact:base:current"); }); });