| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- 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> = {}): 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(<Inspector item={item} tab="business" onTab={vi.fn()} onClose={vi.fn()} detail={detail} />);
- expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["业务详情", "产出与改动", "技术详情"]);
- });
- it("renders the vertical lineage, explicit uncertainty and display mappings", () => {
- render(<DataFlowRenderer data={makeCardData()} />);
- 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(<DataFlowRenderer data={makeCardData({ businessOutputs: [{
- id: "output:decision",
- label: "完整正文",
- value: longText,
- source: { kind: "artifact", label: "候选快照" },
- relation: "persisted-output",
- completeness: "complete",
- }] })} />);
- 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(<DataFlowRenderer loading />);
- expect(screen.getByLabelText("正在加载数据流")).toBeInTheDocument();
- rerender(<DataFlowRenderer error="API 503" />);
- expect(screen.getByText("数据流暂时不可用")).toBeInTheDocument();
- expect(screen.getByText("API 503")).toBeInTheDocument();
- rerender(<DataFlowRenderer missingRef />);
- 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(<Inspector item={item} tab="business" onTab={vi.fn()} onClose={vi.fn()} onArtifact={onArtifact} detail={{ businessSections: [], technical: {} }} />);
- fireEvent.click(screen.getByRole("button", { name: "查看当前主脚本" }));
- expect(onArtifact).toHaveBeenCalledOnce();
- expect(view.finalResult.artifact.snapshotRef).toBe("artifact:base:current");
- });
- });
|