ToolCall.test.tsx (6027B)
1 import { describe, it, expect } from "vitest"; 2 import { render, screen, fireEvent } from "@testing-library/react"; 3 import { ToolCall } from "./ToolCall"; 4 import type { ToolCall as ToolCallType } from "../../store/chat"; 5 6 const baseToolCall: ToolCallType = { 7 id: "tc1", 8 name: "search", 9 args: '{"query":"test"}', 10 result: null, 11 status: "pending", 12 }; 13 14 describe("ToolCall", () => { 15 it("renders tool name in monospace span", () => { 16 const { container } = render(<ToolCall toolCall={baseToolCall} />); 17 const nameSpan = container.querySelector(".font-mono"); 18 expect(nameSpan?.textContent).toBe("search"); 19 }); 20 21 it("renders operation badge", () => { 22 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, name: "read_file" }} />); 23 const badge = container.querySelector(".inline-flex.uppercase"); 24 expect(badge?.textContent).toBe("READ"); 25 }); 26 27 it("renders SEARCH badge for search_files", () => { 28 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, name: "search_files" }} />); 29 const badge = container.querySelector(".inline-flex.uppercase"); 30 expect(badge?.textContent).toBe("SEARCH"); 31 }); 32 33 it("is collapsed by default", () => { 34 render(<ToolCall toolCall={{ ...baseToolCall, result: "found" }} />); 35 expect(screen.queryByText("Arguments")).not.toBeInTheDocument(); 36 expect(screen.queryByText("Result")).not.toBeInTheDocument(); 37 }); 38 39 it("expands to show args and result on click", () => { 40 render(<ToolCall toolCall={{ ...baseToolCall, result: "found it" }} />); 41 fireEvent.click(screen.getByRole("button")); 42 expect(screen.getByText("Arguments")).toBeInTheDocument(); 43 expect(screen.getByText("Result")).toBeInTheDocument(); 44 }); 45 46 it("collapses when clicked again", () => { 47 render(<ToolCall toolCall={{ ...baseToolCall, result: "found it" }} />); 48 fireEvent.click(screen.getByRole("button")); 49 expect(screen.getByText("Arguments")).toBeInTheDocument(); 50 fireEvent.click(screen.getByRole("button")); 51 expect(screen.queryByText("Arguments")).not.toBeInTheDocument(); 52 }); 53 54 it("shows running indicator when status is running", () => { 55 render(<ToolCall toolCall={{ ...baseToolCall, status: "running" }} />); 56 expect(screen.getByText("Running…")).toBeInTheDocument(); 57 }); 58 59 it("shows pending status", () => { 60 render(<ToolCall toolCall={{ ...baseToolCall, status: "pending" }} />); 61 expect(screen.getByText("Pending")).toBeInTheDocument(); 62 }); 63 64 it("shows complete status", () => { 65 render(<ToolCall toolCall={{ ...baseToolCall, status: "complete" }} />); 66 expect(screen.getByText("Done")).toBeInTheDocument(); 67 }); 68 69 it("shows error status", () => { 70 render(<ToolCall toolCall={{ ...baseToolCall, status: "error" }} />); 71 expect(screen.getByText("Error")).toBeInTheDocument(); 72 }); 73 74 it("applies error styling for error status", () => { 75 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, status: "error" }} />); 76 const statusSpan = container.querySelector(".text-red-400"); 77 expect(statusSpan).toBeInTheDocument(); 78 }); 79 80 it("applies running styling", () => { 81 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, status: "running" }} />); 82 const statusSpan = container.querySelector(".text-yellow-400"); 83 expect(statusSpan).toBeInTheDocument(); 84 }); 85 86 it("applies complete styling", () => { 87 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, status: "complete" }} />); 88 const statusSpan = container.querySelector(".text-green-400"); 89 expect(statusSpan).toBeInTheDocument(); 90 }); 91 92 it("applies pending styling", () => { 93 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, status: "pending" }} />); 94 const statusSpan = container.querySelector(".text-gray-400"); 95 expect(statusSpan).toBeInTheDocument(); 96 }); 97 98 it("formats JSON args when expanded", () => { 99 render(<ToolCall toolCall={baseToolCall} />); 100 fireEvent.click(screen.getByRole("button")); 101 expect(screen.getByText(/"query":\s*"test"/)).toBeInTheDocument(); 102 }); 103 104 it("does not show result when result is null", () => { 105 render(<ToolCall toolCall={baseToolCall} />); 106 fireEvent.click(screen.getByRole("button")); 107 expect(screen.getByText("Arguments")).toBeInTheDocument(); 108 expect(screen.queryByText("Result")).not.toBeInTheDocument(); 109 }); 110 111 it("shows result when result is non-null", () => { 112 render(<ToolCall toolCall={{ ...baseToolCall, result: "some output" }} />); 113 fireEvent.click(screen.getByRole("button")); 114 expect(screen.getByText("Result")).toBeInTheDocument(); 115 }); 116 117 it("handles non-JSON args gracefully", () => { 118 render(<ToolCall toolCall={{ ...baseToolCall, args: "not json" }} />); 119 fireEvent.click(screen.getByRole("button")); 120 expect(screen.getByText("not json")).toBeInTheDocument(); 121 }); 122 123 it("arrow rotates when expanded", () => { 124 const { container } = render(<ToolCall toolCall={baseToolCall} />); 125 const arrow = container.querySelector("svg"); 126 expect(arrow).not.toHaveClass("rotate-90"); 127 128 fireEvent.click(screen.getByRole("button")); 129 expect(arrow).toHaveClass("rotate-90"); 130 }); 131 132 it("displays different tool names", () => { 133 const { container } = render(<ToolCall toolCall={{ ...baseToolCall, name: "calculator" }} />); 134 const nameSpan = container.querySelector(".font-mono"); 135 expect(nameSpan?.textContent).toBe("calculator"); 136 }); 137 138 it("running status shows pulse indicator", () => { 139 const { container } = render( 140 <ToolCall toolCall={{ ...baseToolCall, status: "running" }} />, 141 ); 142 const pulse = container.querySelector(".animate-pulse"); 143 expect(pulse).toBeInTheDocument(); 144 }); 145 146 it("non-running status does not show pulse indicator", () => { 147 const { container } = render( 148 <ToolCall toolCall={{ ...baseToolCall, status: "complete" }} />, 149 ); 150 const pulse = container.querySelector(".animate-pulse"); 151 expect(pulse).not.toBeInTheDocument(); 152 }); 153 });