mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
test(frontend): add integration tests for DecomposeGoal, StepItem, and ChatMessagesContainer helpers
Agent-Logs-Url: https://github.com/Significant-Gravitas/AutoGPT/sessions/fbc0ab75-9d2e-4ea5-b9c7-c53fb8605913 Co-authored-by: ntindle <8845353+ntindle@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
35bca7c7ad
commit
225bdfb543
@@ -0,0 +1,316 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildRenderSegments,
|
||||
isCompletedToolPart,
|
||||
isInteractiveToolPart,
|
||||
parseSpecialMarkers,
|
||||
splitReasoningAndResponse,
|
||||
} from "../helpers";
|
||||
import type { MessagePart, RenderSegment } from "../helpers";
|
||||
|
||||
function textPart(text: string): MessagePart {
|
||||
return { type: "text", text } as MessagePart;
|
||||
}
|
||||
|
||||
function toolPart(
|
||||
toolName: string,
|
||||
state: string,
|
||||
output?: unknown,
|
||||
): MessagePart {
|
||||
return {
|
||||
type: `tool-${toolName}`,
|
||||
toolCallId: `call_${toolName}`,
|
||||
toolName,
|
||||
state,
|
||||
output,
|
||||
} as unknown as MessagePart;
|
||||
}
|
||||
|
||||
describe("isCompletedToolPart", () => {
|
||||
it("returns true for output-available tool part", () => {
|
||||
const part = toolPart("some_tool", "output-available");
|
||||
expect(isCompletedToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for output-error tool part", () => {
|
||||
const part = toolPart("some_tool", "output-error");
|
||||
expect(isCompletedToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for input-streaming tool part", () => {
|
||||
const part = toolPart("some_tool", "input-streaming");
|
||||
expect(isCompletedToolPart(part)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for text part", () => {
|
||||
const part = textPart("hello");
|
||||
expect(isCompletedToolPart(part)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInteractiveToolPart", () => {
|
||||
it("returns true for task_decomposition type", () => {
|
||||
const part = toolPart("decompose_goal", "output-available", {
|
||||
type: "task_decomposition",
|
||||
message: "Plan",
|
||||
goal: "Build agent",
|
||||
steps: [],
|
||||
step_count: 0,
|
||||
requires_approval: true,
|
||||
});
|
||||
expect(isInteractiveToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for setup_requirements type", () => {
|
||||
const part = toolPart("run_mcp_tool", "output-available", {
|
||||
type: "setup_requirements",
|
||||
message: "Setup needed",
|
||||
});
|
||||
expect(isInteractiveToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for agent_details type", () => {
|
||||
const part = toolPart("find_agent", "output-available", {
|
||||
type: "agent_details",
|
||||
});
|
||||
expect(isInteractiveToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-interactive output type", () => {
|
||||
const part = toolPart("some_tool", "output-available", {
|
||||
type: "generic_output",
|
||||
});
|
||||
expect(isInteractiveToolPart(part)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when state is not output-available", () => {
|
||||
const part = toolPart("decompose_goal", "input-streaming", {
|
||||
type: "task_decomposition",
|
||||
});
|
||||
expect(isInteractiveToolPart(part)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for non-tool parts", () => {
|
||||
const part = textPart("hello");
|
||||
expect(isInteractiveToolPart(part)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when output is null", () => {
|
||||
const part = toolPart("decompose_goal", "output-available", null);
|
||||
expect(isInteractiveToolPart(part)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles JSON-encoded string output", () => {
|
||||
const part = toolPart(
|
||||
"decompose_goal",
|
||||
"output-available",
|
||||
JSON.stringify({ type: "task_decomposition" }),
|
||||
);
|
||||
expect(isInteractiveToolPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for invalid JSON string output", () => {
|
||||
const part = toolPart(
|
||||
"decompose_goal",
|
||||
"output-available",
|
||||
"not valid json",
|
||||
);
|
||||
expect(isInteractiveToolPart(part)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRenderSegments", () => {
|
||||
it("returns individual segments for custom tool types", () => {
|
||||
const parts = [
|
||||
toolPart("decompose_goal", "output-available", {
|
||||
type: "task_decomposition",
|
||||
}),
|
||||
];
|
||||
const segments = buildRenderSegments(parts);
|
||||
expect(segments).toHaveLength(1);
|
||||
expect(segments[0].kind).toBe("part");
|
||||
});
|
||||
|
||||
it("collapses consecutive generic completed tool parts", () => {
|
||||
const parts = [
|
||||
toolPart("unknown_tool_a", "output-available"),
|
||||
toolPart("unknown_tool_b", "output-available"),
|
||||
];
|
||||
const segments = buildRenderSegments(parts);
|
||||
expect(segments).toHaveLength(1);
|
||||
expect(segments[0].kind).toBe("collapsed-group");
|
||||
if (segments[0].kind === "collapsed-group") {
|
||||
expect(segments[0].parts).toHaveLength(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not collapse custom tool types into groups", () => {
|
||||
const parts = [
|
||||
toolPart("decompose_goal", "output-available", {
|
||||
type: "task_decomposition",
|
||||
}),
|
||||
toolPart("create_agent", "output-available"),
|
||||
];
|
||||
const segments = buildRenderSegments(parts);
|
||||
expect(segments).toHaveLength(2);
|
||||
expect(segments[0].kind).toBe("part");
|
||||
expect(segments[1].kind).toBe("part");
|
||||
});
|
||||
|
||||
it("renders text parts individually", () => {
|
||||
const parts = [textPart("Hello"), textPart("World")];
|
||||
const segments = buildRenderSegments(parts);
|
||||
expect(segments).toHaveLength(2);
|
||||
expect(segments.every((s) => s.kind === "part")).toBe(true);
|
||||
});
|
||||
|
||||
it("handles mixed custom tools, generic tools, and text", () => {
|
||||
const parts = [
|
||||
textPart("Plan:"),
|
||||
toolPart("decompose_goal", "output-available"),
|
||||
toolPart("generic_a", "output-available"),
|
||||
toolPart("generic_b", "output-available"),
|
||||
textPart("Done"),
|
||||
];
|
||||
const segments = buildRenderSegments(parts);
|
||||
|
||||
expect(segments[0].kind).toBe("part");
|
||||
expect(segments[1].kind).toBe("part");
|
||||
expect(segments[2].kind).toBe("collapsed-group");
|
||||
expect(segments[3].kind).toBe("part");
|
||||
});
|
||||
|
||||
it("does not collapse a single generic tool part", () => {
|
||||
const parts = [
|
||||
toolPart("generic_a", "output-available"),
|
||||
];
|
||||
const segments = buildRenderSegments(parts);
|
||||
expect(segments).toHaveLength(1);
|
||||
expect(segments[0].kind).toBe("part");
|
||||
});
|
||||
|
||||
it("preserves baseIndex offset in part segments", () => {
|
||||
const parts = [textPart("Hello")];
|
||||
const segments = buildRenderSegments(parts, 5);
|
||||
expect(segments).toHaveLength(1);
|
||||
if (segments[0].kind === "part") {
|
||||
expect(segments[0].index).toBe(5);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitReasoningAndResponse", () => {
|
||||
it("returns all parts as response when no tools are present", () => {
|
||||
const parts = [textPart("Hello"), textPart("World")];
|
||||
const { reasoning, response } = splitReasoningAndResponse(parts);
|
||||
expect(reasoning).toHaveLength(0);
|
||||
expect(response).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns all parts as response when no text follows the last tool", () => {
|
||||
const parts = [
|
||||
textPart("Thinking..."),
|
||||
toolPart("decompose_goal", "output-available", {
|
||||
type: "task_decomposition",
|
||||
}),
|
||||
];
|
||||
const { reasoning, response } = splitReasoningAndResponse(parts);
|
||||
expect(reasoning).toHaveLength(0);
|
||||
expect(response).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("splits reasoning and response when text follows the last tool", () => {
|
||||
const parts = [
|
||||
textPart("Let me plan this..."),
|
||||
toolPart("decompose_goal", "output-available", {
|
||||
type: "task_decomposition",
|
||||
}),
|
||||
textPart("Here is the plan."),
|
||||
];
|
||||
const { reasoning, response } = splitReasoningAndResponse(parts);
|
||||
expect(reasoning).toHaveLength(1);
|
||||
expect(response).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("pins interactive tool parts to response section", () => {
|
||||
const interactiveTool = toolPart(
|
||||
"decompose_goal",
|
||||
"output-available",
|
||||
{ type: "task_decomposition" },
|
||||
);
|
||||
const parts = [
|
||||
textPart("Thinking..."),
|
||||
interactiveTool,
|
||||
textPart("Here is the summary."),
|
||||
];
|
||||
const { reasoning, response } = splitReasoningAndResponse(parts);
|
||||
|
||||
expect(reasoning).toHaveLength(1);
|
||||
expect(reasoning[0]).toBe(parts[0]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response[0]).toBe(interactiveTool);
|
||||
expect(response[1]).toBe(parts[2]);
|
||||
});
|
||||
|
||||
it("keeps non-interactive tool parts in reasoning", () => {
|
||||
const genericTool = toolPart("find_block", "output-available", {
|
||||
type: "block_list",
|
||||
});
|
||||
const parts = [
|
||||
textPart("Looking for blocks..."),
|
||||
genericTool,
|
||||
textPart("Found them."),
|
||||
];
|
||||
const { reasoning, response } = splitReasoningAndResponse(parts);
|
||||
expect(reasoning).toHaveLength(2);
|
||||
expect(reasoning[1]).toBe(genericTool);
|
||||
expect(response).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSpecialMarkers", () => {
|
||||
it("returns null marker for plain text", () => {
|
||||
const result = parseSpecialMarkers("Hello world");
|
||||
expect(result.markerType).toBeNull();
|
||||
expect(result.cleanText).toBe("Hello world");
|
||||
});
|
||||
|
||||
it("detects error marker", () => {
|
||||
const result = parseSpecialMarkers(
|
||||
"Some preamble [__COPILOT_ERROR_f7a1__] Something went wrong",
|
||||
);
|
||||
expect(result.markerType).toBe("error");
|
||||
expect(result.markerText).toBe("Something went wrong");
|
||||
});
|
||||
|
||||
it("detects retryable error marker", () => {
|
||||
const result = parseSpecialMarkers(
|
||||
"[__COPILOT_RETRYABLE_ERROR_a9c2__] Timeout reached",
|
||||
);
|
||||
expect(result.markerType).toBe("retryable_error");
|
||||
expect(result.markerText).toBe("Timeout reached");
|
||||
});
|
||||
|
||||
it("detects system marker", () => {
|
||||
const result = parseSpecialMarkers(
|
||||
"[__COPILOT_SYSTEM_e3b0__] Session expired",
|
||||
);
|
||||
expect(result.markerType).toBe("system");
|
||||
expect(result.markerText).toBe("Session expired");
|
||||
});
|
||||
|
||||
it("retryable takes precedence over regular error when both present", () => {
|
||||
const text =
|
||||
"[__COPILOT_RETRYABLE_ERROR_a9c2__] Retryable issue [__COPILOT_ERROR_f7a1__] Also error";
|
||||
const result = parseSpecialMarkers(text);
|
||||
expect(result.markerType).toBe("retryable_error");
|
||||
});
|
||||
|
||||
it("strips marker from cleanText", () => {
|
||||
const result = parseSpecialMarkers(
|
||||
"Preamble text [__COPILOT_SYSTEM_e3b0__] System message",
|
||||
);
|
||||
expect(result.cleanText).toBe("Preamble text");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,400 @@
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@/tests/integrations/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { DecomposeGoalTool } from "../DecomposeGoal";
|
||||
import type { TaskDecompositionOutput } from "../helpers";
|
||||
|
||||
const mockOnSend = vi.fn();
|
||||
vi.mock(
|
||||
"../../../components/CopilotChatActionsProvider/useCopilotChatActions",
|
||||
() => ({
|
||||
useCopilotChatActions: () => ({ onSend: mockOnSend }),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/app/api/__generated__/endpoints/chat/chat", () => ({
|
||||
postV2CancelAutoApproveTask: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
step_id: "step_1",
|
||||
description: "Add input block",
|
||||
action: "add_input",
|
||||
block_name: null,
|
||||
status: "pending",
|
||||
},
|
||||
{
|
||||
step_id: "step_2",
|
||||
description: "Add AI summarizer",
|
||||
action: "add_block",
|
||||
block_name: "AI Text Generator",
|
||||
status: "pending",
|
||||
},
|
||||
{
|
||||
step_id: "step_3",
|
||||
description: "Connect blocks",
|
||||
action: "connect_blocks",
|
||||
block_name: null,
|
||||
status: "pending",
|
||||
},
|
||||
];
|
||||
|
||||
const DECOMPOSITION: TaskDecompositionOutput = {
|
||||
type: "task_decomposition",
|
||||
message: "Here's the plan (3 steps):",
|
||||
goal: "Build a news summarizer",
|
||||
steps: STEPS,
|
||||
step_count: 3,
|
||||
requires_approval: true,
|
||||
auto_approve_seconds: 60,
|
||||
created_at: new Date().toISOString(),
|
||||
session_id: "test-session-1",
|
||||
};
|
||||
|
||||
function makePart(
|
||||
state: string,
|
||||
output?: unknown,
|
||||
): { type: string; toolCallId: string; toolName: string; state: string; input?: unknown; output?: unknown } {
|
||||
return {
|
||||
type: "tool-decompose_goal",
|
||||
toolCallId: "call_1",
|
||||
toolName: "decompose_goal",
|
||||
state,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
describe("DecomposeGoalTool", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockOnSend.mockClear();
|
||||
});
|
||||
|
||||
it("renders analyzing animation during input-streaming", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("input-streaming") as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/A/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders error card when state is output-error", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-error") as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(/Failed to analyze the goal/i),
|
||||
).toBeDefined();
|
||||
expect(screen.getByText("Try again")).toBeDefined();
|
||||
});
|
||||
|
||||
it("sends retry message when Try again is clicked on error", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-error") as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByText("Try again"));
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
"Please try decomposing the goal again.",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders error card for error output object", () => {
|
||||
const errorOutput = {
|
||||
type: "error",
|
||||
error: "missing_steps",
|
||||
message: "Please provide at least one step.",
|
||||
};
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", errorOutput) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("Please provide at least one step."),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders the build plan accordion with steps", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/Build Plan — 3 steps/)).toBeDefined();
|
||||
expect(screen.getByText("Build a news summarizer")).toBeDefined();
|
||||
expect(screen.getByText(/Here's the plan/)).toBeDefined();
|
||||
expect(screen.getByText(/1\. Add input block/)).toBeDefined();
|
||||
expect(screen.getByText(/2\. Add AI summarizer/)).toBeDefined();
|
||||
expect(screen.getByText(/3\. Connect blocks/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders block name badges for steps that have them", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("AI Text Generator")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows approve and modify buttons when requires_approval and isLastMessage", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Modify")).toBeDefined();
|
||||
expect(screen.getByText(/Starting in/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("hides action buttons when isLastMessage is false", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("Modify")).toBeNull();
|
||||
expect(
|
||||
screen.getByText(/Review the plan above and approve/),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("hides action buttons when requires_approval is false", () => {
|
||||
const noApproval = { ...DECOMPOSITION, requires_approval: false };
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", noApproval) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("Modify")).toBeNull();
|
||||
});
|
||||
|
||||
it("disables buttons while message is still streaming", () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
isMessageStreaming
|
||||
/>,
|
||||
);
|
||||
const modifyBtn = screen.getByText("Modify").closest("button");
|
||||
expect(modifyBtn?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("sends approval message when approve button is clicked", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
const startBtn = screen.getByText(/Starting in/).closest("button");
|
||||
expect(startBtn).toBeDefined();
|
||||
fireEvent.click(startBtn!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
"Approved. Please build the agent.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not send duplicate approval on second click", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
const startBtn = screen.getByText(/Starting in/).closest("button");
|
||||
fireEvent.click(startBtn!);
|
||||
fireEvent.click(startBtn!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("enters edit mode when Modify is clicked", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
const textareas = screen.getAllByPlaceholderText("Step description");
|
||||
expect(textareas.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels auto-approve on the server when Modify is clicked", async () => {
|
||||
const { postV2CancelAutoApproveTask } = await import(
|
||||
"@/app/api/__generated__/endpoints/chat/chat"
|
||||
);
|
||||
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postV2CancelAutoApproveTask).toHaveBeenCalledWith(
|
||||
"test-session-1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("allows editing step descriptions in edit mode", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(3);
|
||||
});
|
||||
|
||||
const textareas = screen.getAllByPlaceholderText("Step description");
|
||||
fireEvent.change(textareas[0], {
|
||||
target: { value: "Fetch RSS feed" },
|
||||
});
|
||||
|
||||
expect(
|
||||
(screen.getAllByPlaceholderText("Step description")[0] as HTMLTextAreaElement).value,
|
||||
).toBe("Fetch RSS feed");
|
||||
});
|
||||
|
||||
it("allows deleting steps in edit mode", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(3);
|
||||
});
|
||||
|
||||
const removeButtons = screen.getAllByLabelText("Remove step");
|
||||
fireEvent.click(removeButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("allows inserting new steps in edit mode", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(3);
|
||||
});
|
||||
|
||||
const insertButtons = screen.getAllByLabelText("Insert step here");
|
||||
fireEvent.click(insertButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
it("sends modified steps message when approve is clicked in edit mode", async () => {
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Modify"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Step description").length).toBe(3);
|
||||
});
|
||||
|
||||
const textareas = screen.getAllByPlaceholderText("Step description");
|
||||
fireEvent.change(textareas[0], {
|
||||
target: { value: "Fetch RSS feed" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Approve"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Approved with modifications"),
|
||||
);
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Fetch RSS feed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders countdown timer in the approve button", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(DECOMPOSITION.created_at!));
|
||||
|
||||
render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("output-available", DECOMPOSITION) as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("60")).toBeDefined();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("renders nothing pending when output is not yet available", () => {
|
||||
const { container } = render(
|
||||
<DecomposeGoalTool
|
||||
part={makePart("input-available") as any}
|
||||
isLastMessage
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(".py-2")).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { render, screen } from "@/tests/integrations/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { StepItem } from "../StepItem";
|
||||
|
||||
describe("StepItem", () => {
|
||||
it("renders step number and description", () => {
|
||||
render(
|
||||
<StepItem index={0} description="Add input block" status="pending" />,
|
||||
);
|
||||
expect(screen.getByText("1. Add input block")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders block name when provided", () => {
|
||||
render(
|
||||
<StepItem
|
||||
index={1}
|
||||
description="Add AI summarizer"
|
||||
blockName="AI Text Generator"
|
||||
status="pending"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("AI Text Generator")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render block name when null", () => {
|
||||
render(
|
||||
<StepItem
|
||||
index={0}
|
||||
description="Connect blocks"
|
||||
blockName={null}
|
||||
status="pending"
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("AI Text Generator")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders pending icon by default", () => {
|
||||
render(<StepItem index={0} description="Step" status="pending" />);
|
||||
expect(screen.getByLabelText("pending")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders completed icon for completed status", () => {
|
||||
render(<StepItem index={0} description="Step" status="completed" />);
|
||||
expect(screen.getByLabelText("completed")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders in-progress icon for in_progress status", () => {
|
||||
render(<StepItem index={0} description="Step" status="in_progress" />);
|
||||
expect(screen.getByLabelText("in progress")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders failed icon for failed status", () => {
|
||||
render(<StepItem index={0} description="Step" status="failed" />);
|
||||
expect(screen.getByLabelText("failed")).toBeDefined();
|
||||
});
|
||||
|
||||
it("uses zero-based index to render 1-based step number", () => {
|
||||
render(<StepItem index={4} description="Fifth step" status="pending" />);
|
||||
expect(screen.getByText("5. Fifth step")).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user