Compare commits

...

2 Commits

Author SHA1 Message Date
Cursor Agent
696c3bbbce feat: Add webhook trigger test and snapshot
Co-authored-by: nicholas.tindle <nicholas.tindle@agpt.co>
2025-12-18 17:40:43 +00:00
itsababseh
0861e4a759 fix: wrap webhook URL in trigger block 2025-12-18 16:37:03 +00:00
4 changed files with 176 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ import {
BlockIOSubSchema,
BlockUIType,
Category,
Node,
NodeExecutionResult,
} from "@/lib/autogpt-server-api";
import {
@@ -81,6 +82,7 @@ export type CustomNodeData = {
outputSchema: BlockIORootSchema;
hardcodedValues: { [key: string]: any };
connections: ConnectionData;
webhook?: Node["webhook"];
isOutputOpen: boolean;
status?: NodeExecutionResult["status"];
/** executionResults contains outputs across multiple executions
@@ -910,6 +912,39 @@ export const CustomNode = React.memo(
</span>
</AlertDescription>
</Alert>
{data.uiType === BlockUIType.WEBHOOK_MANUAL && (
<>
{data.webhook ? (
<div className="nodrag mr-5 flex flex-col gap-1">
Webhook URL:
<div className="flex gap-2 rounded-md bg-gray-50 p-2">
<code className="select-all break-all text-sm">
{data.webhook.url}
</code>
<Button
variant="outline"
size="icon"
className="size-7 flex-none"
onClick={() =>
data.webhook &&
navigator.clipboard.writeText(
data.webhook.url,
)
}
title="Copy webhook URL"
>
<CopyIcon className="size-4" />
</Button>
</div>
</div>
) : (
<p className="italic text-gray-500">
(A Webhook URL will be generated when you save the
agent)
</p>
)}
</>
)}
<div className="pointer-events-none opacity-50">
{generateInputHandles(data.inputSchema, data.uiType)}
</div>

View File

@@ -0,0 +1,124 @@
"use client";
import { ReactFlow, ReactFlowProvider } from "@xyflow/react";
import {
CustomNode,
type CustomNode as FlowNode,
type CustomNodeData,
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
import { BuilderContext } from "@/app/(platform)/build/components/legacy-builder/Flow/Flow";
import {
BlockUIType,
type Category,
type Node as GraphNode,
} from "@/lib/autogpt-server-api";
import { useMemo } from "react";
const mockCategories: Category[] = [
{
category: "triggers",
description: "Trigger block",
},
];
const mockWebhook: NonNullable<GraphNode["webhook"]> = {
id: "webhook-manual-123",
url: "https://hooks.autogpt.io/very/long/path/that/should/wrap/properly/when/rendered/in/the/manual/webhook/block/and-demonstrate-that-break-all-is-working/callback?id=1234567890abcdefghijklmnopqrstuvwxyz",
provider: "http",
credentials_id: "",
webhook_type: "manual",
resource: "generic",
events: ["POST"],
secret: "****",
config: {},
};
const mockNodeData: CustomNodeData = {
blockType: "WebhookManualBlock",
blockCosts: [],
title: "Generic Webhook (Manual)",
description: "Trigger flows manually via incoming POST requests.",
categories: mockCategories,
inputSchema: {
type: "object",
properties: {},
required: [],
},
outputSchema: {
type: "object",
properties: {},
required: [],
},
hardcodedValues: {},
connections: [],
webhook: mockWebhook,
isOutputOpen: false,
uiType: BlockUIType.WEBHOOK_MANUAL,
block_id: "manual-webhook-block",
executionResults: [],
errors: {},
metadata: {},
};
const mockBuilderContext = {
libraryAgent: null,
visualizeBeads: "no" as const,
setIsAnyModalOpen: () => undefined,
getNextNodeId: () => "mock-node-id",
getNodeTitle: () => mockNodeData.title,
};
export default function WebhookTriggerPreviewPage() {
const nodeData = useMemo(
() => ({
...mockNodeData,
inputSchema: { ...mockNodeData.inputSchema },
outputSchema: { ...mockNodeData.outputSchema },
hardcodedValues: { ...mockNodeData.hardcodedValues },
connections: [...mockNodeData.connections],
categories: mockNodeData.categories.map((category) => ({ ...category })),
executionResults: mockNodeData.executionResults
? [...mockNodeData.executionResults]
: mockNodeData.executionResults,
webhook: mockNodeData.webhook
? { ...mockNodeData.webhook }
: mockNodeData.webhook,
}),
[],
);
const nodes = useMemo<FlowNode[]>(
() => [
{
id: "manual-webhook-node",
type: "custom",
position: { x: 0, y: 0 },
data: nodeData,
},
],
[nodeData],
);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-8">
<ReactFlowProvider>
<BuilderContext.Provider value={mockBuilderContext}>
<div className="h-[600px] w-[640px] rounded-2xl bg-white p-6 shadow-lg">
<ReactFlow
nodes={nodes}
edges={[]}
nodeTypes={{ custom: CustomNode }}
panOnScroll={false}
zoomOnScroll={false}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
fitView
fitViewOptions={{ padding: 0.2 }}
/>
</div>
</BuilderContext.Provider>
</ReactFlowProvider>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { expect, test } from "@playwright/test";
const ROUTE = "/playwright/webhook-trigger";
test.describe("Webhook manual block", () => {
test("wraps long webhook URLs without overflow", async ({ page }) => {
await page.goto(ROUTE);
const block = page.locator('[data-blockid="manual-webhook-block"]');
await expect(block).toBeVisible();
const webhookUrl = block.locator("code");
await expect(webhookUrl).toContainText("https://hooks.autogpt.io/");
await expect(block).toHaveScreenshot("webhook-manual-block.png");
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB