inspector-comparison.test.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { fireEvent, render, screen } from "@testing-library/react";
  2. import { describe, expect, it, vi } from "vitest";
  3. import { InspectorComparison } from "@/components/inspector/InspectorComparison";
  4. import { Inspector } from "@/components/inspector/Inspector";
  5. import { storyItem } from "@/lib/layout";
  6. import { makeStory } from "@/tests/fixtures";
  7. import type { InspectorWorkbenchProjection, SourceBinding } from "@/lib/types";
  8. const binding: SourceBinding = {
  9. id: "result-1",
  10. sourceId: "event:4607",
  11. role: "output",
  12. selector: { kind: "json-pointer", path: "/output/content/results/0" },
  13. transform: { kind: "direct" },
  14. evidence: { availability: "returned-to-agent", adoption: "not-recorded", confidence: "exact" },
  15. resolution: "resolved",
  16. };
  17. const payload: InspectorWorkbenchProjection = {
  18. schemaVersion: "inspector-source-v2",
  19. detailRef: "event:4607",
  20. cardKind: "query",
  21. completeness: "complete",
  22. businessProjection: {
  23. detailKind: "activity",
  24. businessSections: [{ id: "results", title: "返回结果", items: [{ label: "Case 1", value: "命中内容" }] }],
  25. },
  26. modules: [{
  27. id: "results",
  28. title: "返回结果",
  29. businessPath: "/businessSections/0",
  30. presentation: "items",
  31. rows: [{ id: "case-1", label: "Case 1", businessSelector: "/businessSections/0/items/0/value", bindingIds: [binding.id] }],
  32. bindings: [binding],
  33. runtimeRefs: ["event:4607"],
  34. defaultOpen: true,
  35. }],
  36. sources: {
  37. "event:4607": {
  38. id: "event:4607",
  39. kind: "runtime-event",
  40. label: "search_script_decode_case",
  41. locator: { table: "script_build_event + script_build_event_body", eventId: 4607, eventName: "search_script_decode_case" },
  42. status: "ok",
  43. completeness: "complete",
  44. resultState: "present",
  45. selectedValues: [{ bindingId: binding.id, path: "/output/content/results/0", value: "命中内容" }],
  46. rawRecord: { output: { content: { results: ["命中内容"] } } },
  47. },
  48. },
  49. notices: [],
  50. };
  51. describe("InspectorComparison", () => {
  52. it("returns from the source comparison to the readable business Inspector", () => {
  53. const onModeChange = vi.fn();
  54. const item = storyItem(makeStory("event:4607", "planning-analysis"), 1);
  55. render(<Inspector item={item} mode="lineage" onModeChange={onModeChange} onClose={vi.fn()} workbench={payload} />);
  56. fireEvent.click(screen.getByRole("button", { name: "返回业务详情" }));
  57. expect(onModeChange).toHaveBeenCalledWith("business");
  58. });
  59. it("renders business fields from the same projection used by the readable Inspector", () => {
  60. const planPayload: InspectorWorkbenchProjection = {
  61. ...payload,
  62. detailRef: "round:1:plan",
  63. cardKind: "implementation-plan",
  64. businessProjection: {
  65. detailKind: "agent-decision",
  66. blocks: [{
  67. type: "implementation-plan",
  68. title: "本轮实施路线",
  69. mode: "竞争",
  70. reasoning: "比较两条路线。",
  71. routes: [{ pathIndex: 1, target: "完整段落骨架", method: "产生脉络", emphasis: "先保证结构清楚" }],
  72. }],
  73. },
  74. modules: [{
  75. id: "block-1",
  76. title: "本轮实施路线",
  77. businessPath: "/blocks/0",
  78. presentation: "plan",
  79. rows: [
  80. { id: "mode", label: "规划方式", businessSelector: "/blocks/0/mode", bindingIds: [binding.id] },
  81. { id: "target", label: "路线 1 · 作用范围", businessSelector: "/blocks/0/routes/0/target", bindingIds: [binding.id] },
  82. { id: "method", label: "路线 1 · 实施方法", businessSelector: "/blocks/0/routes/0/method", bindingIds: [binding.id] },
  83. { id: "emphasis", label: "路线 1 · 路线侧重", businessSelector: "/blocks/0/routes/0/emphasis", bindingIds: [binding.id] },
  84. ],
  85. bindings: [binding],
  86. runtimeRefs: [],
  87. defaultOpen: true,
  88. }],
  89. };
  90. const { container } = render(<InspectorComparison data={planPayload} />);
  91. expect(screen.getByText("竞争")).toBeInTheDocument();
  92. expect(screen.getByText("完整段落骨架")).toBeInTheDocument();
  93. expect(screen.getByText("产生脉络")).toBeInTheDocument();
  94. expect(screen.getByText("先保证结构清楚")).toBeInTheDocument();
  95. expect(container.querySelector(".comparisonItemBusiness")?.textContent).not.toContain("pathIndex");
  96. });
  97. it("renders one physical row across all three columns", () => {
  98. const { container } = render(<InspectorComparison data={payload} />);
  99. expect(screen.getByText("业务详情中的内容")).toBeInTheDocument();
  100. expect(screen.getByText("这一项的数据依据")).toBeInTheDocument();
  101. expect(screen.getByText("对应的原始字段或原文")).toBeInTheDocument();
  102. expect(screen.getByText("返回结果")).toBeInTheDocument();
  103. expect(screen.getAllByText("S1").length).toBeGreaterThanOrEqual(2);
  104. expect(screen.getByText("已返回给 Agent,是否采用未记录")).toBeInTheDocument();
  105. expect(screen.getByText("script_build_event + script_build_event_body")).toBeInTheDocument();
  106. expect(container.querySelectorAll(".comparisonItemRow")).toHaveLength(1);
  107. });
  108. it("shows the exact selected source value without opening raw JSON", () => {
  109. render(<InspectorComparison data={payload} />);
  110. expect(screen.getAllByText("命中内容").length).toBeGreaterThanOrEqual(2);
  111. expect(screen.getByText("完整原始记录")).toBeInTheDocument();
  112. expect(screen.getByText("/output/content/results/0").closest("details")).not.toHaveAttribute("open");
  113. expect(screen.queryByText("found")).not.toBeInTheDocument();
  114. });
  115. it("keeps structured business rows readable instead of exposing object keys", () => {
  116. const decisionPayload: InspectorWorkbenchProjection = {
  117. ...payload,
  118. cardKind: "data-decision",
  119. businessProjection: {
  120. detailKind: "agent-decision",
  121. blocks: [{
  122. type: "items",
  123. title: "参与判断",
  124. items: [{ label: "解构 Case", value: "返回了 2 条可用案例", note: "已读取" }],
  125. }],
  126. },
  127. modules: [{
  128. ...payload.modules[0],
  129. id: "block-1",
  130. title: "参与判断",
  131. businessPath: "/blocks/0",
  132. rows: [{ id: "source-1", label: "解构 Case", businessSelector: "/blocks/0/items/0", bindingIds: [binding.id] }],
  133. }],
  134. };
  135. const { container } = render(<InspectorComparison data={decisionPayload} />);
  136. const business = container.querySelector(".comparisonItemBusiness");
  137. expect(business).toHaveTextContent("解构 Case");
  138. expect(business).toHaveTextContent("返回了 2 条可用案例");
  139. expect(business).toHaveTextContent("说明");
  140. expect(business?.textContent).not.toMatch(/\blabel\b|\bvalue\b|\bnote\b/);
  141. });
  142. it("synchronizes binding highlight within the already aligned row", () => {
  143. const onChange = vi.fn();
  144. const { container } = render(<InspectorComparison data={payload} onActiveBindingChange={onChange} />);
  145. fireEvent.click(screen.getByRole("button", { name: /已返回给 Agent/ }));
  146. expect(container.querySelectorAll('[data-binding-id="result-1"]').length).toBeGreaterThanOrEqual(2);
  147. expect(container.querySelector(".comparisonItemRow.is-active")).toBeInTheDocument();
  148. expect(onChange).toHaveBeenLastCalledWith("result-1");
  149. });
  150. it("unmounts module contents when collapsed", () => {
  151. render(<InspectorComparison data={payload} />);
  152. fireEvent.click(screen.getByRole("button", { name: /返回结果/ }));
  153. expect(screen.queryByText("命中内容")).not.toBeInTheDocument();
  154. });
  155. it("explains a successful empty result instead of calling it missing", () => {
  156. const empty: InspectorWorkbenchProjection = {
  157. ...payload,
  158. sources: {
  159. "event:4607": { ...payload.sources["event:4607"], resultState: "empty", rawRecord: [] },
  160. },
  161. };
  162. render(<InspectorComparison data={empty} />);
  163. expect(screen.getByText("查询完成,返回 0 条")).toBeInTheDocument();
  164. expect(screen.getByText("这不是记录缺失。")).toBeInTheDocument();
  165. });
  166. it("hands focus control to an upper prompt or artifact overlay", () => {
  167. const item = storyItem(makeStory("event:4607", "planning-analysis"), 1);
  168. const onClose = vi.fn();
  169. const view = (topOverlayOpen: boolean) => <><button type="button" data-testid="upper-overlay-control">上层弹窗按钮</button><Inspector item={item} onClose={onClose} workbench={payload} topOverlayOpen={topOverlayOpen} /></>;
  170. const { rerender } = render(view(false));
  171. const outside = screen.getByTestId("upper-overlay-control");
  172. outside.focus();
  173. expect(screen.getByRole("dialog")).toContainElement(document.activeElement as HTMLElement);
  174. rerender(view(true));
  175. const upperOverlayControl = screen.getByTestId("upper-overlay-control");
  176. upperOverlayControl.focus();
  177. expect(document.activeElement).toBe(upperOverlayControl);
  178. });
  179. });