279 lines
9.4 KiB
TypeScript
279 lines
9.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import React from "react";
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const handleCalls: Array<{ type: string; id?: string }> = [];
|
|
|
|
vi.mock("@/components/canvas/nodes/base-node-wrapper", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => React.createElement("div", null, children),
|
|
}));
|
|
|
|
vi.mock("@xyflow/react", () => ({
|
|
Handle: ({ type, id }: { type: string; id?: string }) => {
|
|
handleCalls.push({ type, id });
|
|
return React.createElement("div", {
|
|
"data-handle-type": type,
|
|
"data-handle-id": id,
|
|
});
|
|
},
|
|
Position: { Left: "left", Right: "right" },
|
|
}));
|
|
|
|
const translations: Record<string, string> = {
|
|
"agentOutputNode.defaultTitle": "Agent output",
|
|
"agentOutputNode.plannedOutputDefaultTitle": "Planned output",
|
|
"agentOutputNode.skeletonBadge": "Skeleton",
|
|
"agentOutputNode.plannedOutputLabel": "Planned output",
|
|
"agentOutputNode.channelLabel": "Channel",
|
|
"agentOutputNode.artifactTypeLabel": "Artifact type",
|
|
"agentOutputNode.sectionsLabel": "Sections",
|
|
"agentOutputNode.metadataLabel": "Metadata",
|
|
"agentOutputNode.qualityChecksLabel": "Quality checks",
|
|
"agentOutputNode.previewLabel": "Preview",
|
|
"agentOutputNode.previewFallback": "No preview available",
|
|
"agentOutputNode.emptyValue": "-",
|
|
"agentOutputNode.bodyLabel": "Body",
|
|
"agentOutputNode.plannedContent": "Planned content",
|
|
};
|
|
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: (namespace?: string) =>
|
|
(key: string, values?: Record<string, unknown>) => {
|
|
const fullKey = namespace ? `${namespace}.${key}` : key;
|
|
let text = translations[fullKey] ?? key;
|
|
if (values) {
|
|
for (const [name, value] of Object.entries(values)) {
|
|
text = text.replaceAll(`{${name}}`, String(value));
|
|
}
|
|
}
|
|
return text;
|
|
},
|
|
}));
|
|
|
|
import AgentOutputNode from "@/components/canvas/nodes/agent-output-node";
|
|
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("AgentOutputNode", () => {
|
|
let container: HTMLDivElement | null = null;
|
|
let root: Root | null = null;
|
|
|
|
beforeEach(() => {
|
|
handleCalls.length = 0;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (root) {
|
|
act(() => {
|
|
root?.unmount();
|
|
});
|
|
}
|
|
container?.remove();
|
|
container = null;
|
|
root = null;
|
|
});
|
|
|
|
it("renders structured output with artifact meta, sections, metadata, quality checks, and preview fallback", async () => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(AgentOutputNode, {
|
|
id: "agent-output-1",
|
|
selected: false,
|
|
dragging: false,
|
|
draggable: true,
|
|
selectable: true,
|
|
deletable: true,
|
|
zIndex: 1,
|
|
isConnectable: true,
|
|
type: "agent-output",
|
|
data: {
|
|
title: "Instagram Caption",
|
|
channel: "instagram-feed",
|
|
artifactType: "caption-pack",
|
|
previewText: "A short punchy caption with hashtags",
|
|
sections: [
|
|
{ id: "hook", label: "Hook", content: "Launch day is here." },
|
|
{ id: "body", label: "Body", content: "Built for modern teams." },
|
|
],
|
|
metadata: {
|
|
objective: "Drive signups",
|
|
tags: ["launch", "product"],
|
|
},
|
|
qualityChecks: ["channel-fit", "cta-present"],
|
|
body: "Legacy body fallback",
|
|
_status: "done",
|
|
} as Record<string, unknown>,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Instagram Caption");
|
|
expect(container.textContent).toContain("instagram-feed");
|
|
expect(container.textContent).toContain("caption-pack");
|
|
expect(container.textContent).toContain("Sections");
|
|
expect(container.textContent).toContain("Hook");
|
|
expect(container.textContent).toContain("Launch day is here.");
|
|
expect(container.textContent).toContain("Metadata");
|
|
expect(container.textContent).toContain("objective");
|
|
expect(container.textContent).toContain("Drive signups");
|
|
expect(container.textContent).toContain("Quality checks");
|
|
expect(container.textContent).toContain("channel-fit");
|
|
expect(container.textContent).toContain("Preview");
|
|
expect(container.textContent).toContain("A short punchy caption with hashtags");
|
|
expect(container.querySelector('[data-testid="agent-output-meta-strip"]')).not.toBeNull();
|
|
expect(container.querySelector('[data-testid="agent-output-sections"]')).not.toBeNull();
|
|
expect(container.querySelector('[data-testid="agent-output-metadata"]')).not.toBeNull();
|
|
expect(container.querySelector('[data-testid="agent-output-quality-checks"]')).not.toBeNull();
|
|
expect(container.querySelector('[data-testid="agent-output-preview"]')).not.toBeNull();
|
|
});
|
|
|
|
it("renders parseable json body in a pretty-printed code block", async () => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(AgentOutputNode, {
|
|
id: "agent-output-4",
|
|
selected: false,
|
|
dragging: false,
|
|
draggable: true,
|
|
selectable: true,
|
|
deletable: true,
|
|
zIndex: 1,
|
|
isConnectable: true,
|
|
type: "agent-output",
|
|
data: {
|
|
title: "JSON output",
|
|
channel: "api",
|
|
artifactType: "payload",
|
|
body: '{"post":"Hello","tags":["launch","news"]}',
|
|
_status: "done",
|
|
} as Record<string, unknown>,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
const jsonBody = container.querySelector('[data-testid="agent-output-json-body"]');
|
|
expect(jsonBody).not.toBeNull();
|
|
expect(jsonBody?.textContent).toContain('"post": "Hello"');
|
|
expect(jsonBody?.textContent).toContain('"tags": [');
|
|
expect(container.querySelector('[data-testid="agent-output-text-body"]')).toBeNull();
|
|
});
|
|
|
|
it("falls back to legacy text body when structured fields are absent", async () => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(AgentOutputNode, {
|
|
id: "agent-output-legacy",
|
|
selected: false,
|
|
dragging: false,
|
|
draggable: true,
|
|
selectable: true,
|
|
deletable: true,
|
|
zIndex: 1,
|
|
isConnectable: true,
|
|
type: "agent-output",
|
|
data: {
|
|
title: "Legacy output",
|
|
channel: "linkedin",
|
|
artifactType: "post",
|
|
body: "Legacy body content",
|
|
} as Record<string, unknown>,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(container.querySelector('[data-testid="agent-output-text-body"]')).not.toBeNull();
|
|
expect(container.textContent).toContain("Legacy body content");
|
|
expect(container.querySelector('[data-testid="agent-output-sections"]')).toBeNull();
|
|
});
|
|
|
|
it("renders input-only handle agent-output-in", async () => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(AgentOutputNode, {
|
|
id: "agent-output-2",
|
|
selected: false,
|
|
dragging: false,
|
|
draggable: true,
|
|
selectable: true,
|
|
deletable: true,
|
|
zIndex: 1,
|
|
isConnectable: true,
|
|
type: "agent-output",
|
|
data: {
|
|
title: "LinkedIn Post",
|
|
channel: "linkedin",
|
|
artifactType: "post",
|
|
body: "Body",
|
|
} as Record<string, unknown>,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(handleCalls).toEqual([{ type: "target", id: "agent-output-in" }]);
|
|
});
|
|
|
|
it("renders skeleton mode with counter and placeholder", async () => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(AgentOutputNode, {
|
|
id: "agent-output-3",
|
|
selected: false,
|
|
dragging: false,
|
|
draggable: true,
|
|
selectable: true,
|
|
deletable: true,
|
|
zIndex: 1,
|
|
isConnectable: true,
|
|
type: "agent-output",
|
|
data: {
|
|
title: "Planned headline",
|
|
channel: "linkedin",
|
|
artifactType: "post",
|
|
isSkeleton: true,
|
|
stepIndex: 1,
|
|
stepTotal: 4,
|
|
} as Record<string, unknown>,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Skeleton");
|
|
expect(container.textContent).toContain("2/4");
|
|
expect(container.querySelector('[data-testid="agent-output-skeleton-body"]')).not.toBeNull();
|
|
expect(handleCalls).toEqual([{ type: "target", id: "agent-output-in" }]);
|
|
});
|
|
});
|