diff --git a/src/components/__tests__/BaseNode.test.tsx b/src/components/__tests__/BaseNode.test.tsx
new file mode 100644
index 00000000..8101ae0a
--- /dev/null
+++ b/src/components/__tests__/BaseNode.test.tsx
@@ -0,0 +1,564 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { BaseNode } from "@/components/nodes/BaseNode";
+import { ReactFlowProvider } from "@xyflow/react";
+
+// Mock the workflow store
+const mockUseWorkflowStore = vi.fn();
+
+vi.mock("@/store/workflowStore", () => ({
+ useWorkflowStore: (selector: (state: unknown) => unknown) => mockUseWorkflowStore(selector),
+}));
+
+// Mock useReactFlow
+const mockGetNodes = vi.fn(() => []);
+const mockSetNodes = vi.fn();
+
+vi.mock("@xyflow/react", async () => {
+ const actual = await vi.importActual("@xyflow/react");
+ return {
+ ...actual,
+ useReactFlow: () => ({
+ getNodes: mockGetNodes,
+ setNodes: mockSetNodes,
+ }),
+ };
+});
+
+// Wrapper component for React Flow context
+function TestWrapper({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+
+describe("BaseNode", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Default mock implementation
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ currentNodeId: null,
+ groups: {},
+ nodes: [],
+ };
+ return selector(state);
+ });
+ });
+
+ const defaultProps = {
+ id: "test-node-1",
+ title: "Test Node",
+ children:
Test Content
,
+ };
+
+ describe("Basic Rendering", () => {
+ it("should render the title", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Test Node")).toBeInTheDocument();
+ });
+
+ it("should render children content", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("test-children")).toBeInTheDocument();
+ expect(screen.getByText("Test Content")).toBeInTheDocument();
+ });
+
+ it("should apply custom className", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const nodeDiv = container.querySelector(".custom-class");
+ expect(nodeDiv).toBeInTheDocument();
+ });
+
+ it("should render custom title with default title", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("My Custom Name - Test Node")).toBeInTheDocument();
+ });
+
+ it("should render titlePrefix when provided", () => {
+ render(
+
+ PREFIX}
+ />
+
+ );
+
+ expect(screen.getByTestId("title-prefix")).toBeInTheDocument();
+ });
+
+ it("should render headerAction when provided", () => {
+ render(
+
+ Action}
+ />
+
+ );
+
+ expect(screen.getByTestId("header-action")).toBeInTheDocument();
+ });
+ });
+
+ describe("Title Editing", () => {
+ it("should enter edit mode when title is clicked", () => {
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Test Node");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ expect(input).toBeInTheDocument();
+ });
+
+ it("should update title value when typing", () => {
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Test Node");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ fireEvent.change(input, { target: { value: "New Title" } });
+
+ expect(input).toHaveValue("New Title");
+ });
+
+ it("should submit title on Enter and call onCustomTitleChange", () => {
+ const mockOnCustomTitleChange = vi.fn();
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Test Node");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ fireEvent.change(input, { target: { value: "New Title" } });
+ fireEvent.keyDown(input, { key: "Enter" });
+
+ expect(mockOnCustomTitleChange).toHaveBeenCalledWith("New Title");
+ });
+
+ it("should cancel editing on Escape and revert to original value", () => {
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Original - Test Node");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ fireEvent.change(input, { target: { value: "Changed" } });
+ fireEvent.keyDown(input, { key: "Escape" });
+
+ // Should exit edit mode and show original title
+ expect(screen.getByText("Original - Test Node")).toBeInTheDocument();
+ });
+
+ it("should submit title on blur", () => {
+ const mockOnCustomTitleChange = vi.fn();
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("Test Node");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ fireEvent.change(input, { target: { value: "Blurred Title" } });
+ fireEvent.blur(input);
+
+ expect(mockOnCustomTitleChange).toHaveBeenCalledWith("Blurred Title");
+ });
+ });
+
+ describe("Comment Functionality", () => {
+ it("should render empty comment button when no comment exists", () => {
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Add comment");
+ expect(commentButton).toBeInTheDocument();
+ });
+
+ it("should render filled comment button when comment exists", () => {
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Edit comment");
+ expect(commentButton).toBeInTheDocument();
+ });
+
+ it("should open comment popover when button is clicked", () => {
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Add comment");
+ fireEvent.click(commentButton);
+
+ const textarea = screen.getByPlaceholderText("Add a comment...");
+ expect(textarea).toBeInTheDocument();
+ });
+
+ it("should show existing comment in textarea", () => {
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Edit comment");
+ fireEvent.click(commentButton);
+
+ const textarea = screen.getByPlaceholderText("Add a comment...");
+ expect(textarea).toHaveValue("Existing comment");
+ });
+
+ it("should call onCommentChange when Save is clicked", () => {
+ const mockOnCommentChange = vi.fn();
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Add comment");
+ fireEvent.click(commentButton);
+
+ const textarea = screen.getByPlaceholderText("Add a comment...");
+ fireEvent.change(textarea, { target: { value: "New comment" } });
+
+ const saveButton = screen.getByText("Save");
+ fireEvent.click(saveButton);
+
+ expect(mockOnCommentChange).toHaveBeenCalledWith("New comment");
+ });
+
+ it("should close popover and revert on Cancel", () => {
+ const mockOnCommentChange = vi.fn();
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Edit comment");
+ fireEvent.click(commentButton);
+
+ const textarea = screen.getByPlaceholderText("Add a comment...");
+ fireEvent.change(textarea, { target: { value: "Changed" } });
+
+ const cancelButton = screen.getByText("Cancel");
+ fireEvent.click(cancelButton);
+
+ // Popover should be closed
+ expect(screen.queryByPlaceholderText("Add a comment...")).not.toBeInTheDocument();
+ expect(mockOnCommentChange).not.toHaveBeenCalled();
+ });
+
+ it("should close popover on Escape key", () => {
+ render(
+
+
+
+ );
+
+ const commentButton = screen.getByTitle("Add comment");
+ fireEvent.click(commentButton);
+
+ const textarea = screen.getByPlaceholderText("Add a comment...");
+ fireEvent.keyDown(textarea, { key: "Escape" });
+
+ expect(screen.queryByPlaceholderText("Add a comment...")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Expand Button", () => {
+ it("should not render expand button when onExpand is not provided", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTitle("Expand editor")).not.toBeInTheDocument();
+ });
+
+ it("should render expand button when onExpand is provided", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTitle("Expand editor")).toBeInTheDocument();
+ });
+
+ it("should call onExpand when expand button is clicked", () => {
+ const mockOnExpand = vi.fn();
+ render(
+
+
+
+ );
+
+ const expandButton = screen.getByTitle("Expand editor");
+ fireEvent.click(expandButton);
+
+ expect(mockOnExpand).toHaveBeenCalled();
+ });
+ });
+
+ describe("Run Button", () => {
+ it("should not render run button when onRun is not provided", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTitle("Run this node")).not.toBeInTheDocument();
+ });
+
+ it("should render run button when onRun is provided", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTitle("Run this node")).toBeInTheDocument();
+ });
+
+ it("should call onRun when run button is clicked", () => {
+ const mockOnRun = vi.fn();
+ render(
+
+
+
+ );
+
+ const runButton = screen.getByTitle("Run this node");
+ fireEvent.click(runButton);
+
+ expect(mockOnRun).toHaveBeenCalled();
+ });
+
+ it("should disable run button when isExecuting is true", () => {
+ render(
+
+
+
+ );
+
+ const runButton = screen.getByTitle("Run this node");
+ expect(runButton).toBeDisabled();
+ });
+
+ it("should not call onRun when button is disabled", () => {
+ const mockOnRun = vi.fn();
+ render(
+
+
+
+ );
+
+ const runButton = screen.getByTitle("Run this node");
+ fireEvent.click(runButton);
+
+ expect(mockOnRun).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Lock Badge", () => {
+ it("should not render lock badge when node is not in a locked group", () => {
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ currentNodeId: null,
+ groups: { "group-1": { id: "group-1", locked: false } },
+ nodes: [{ id: "test-node-1", groupId: "group-1" }],
+ };
+ return selector(state);
+ });
+
+ render(
+
+
+
+ );
+
+ expect(
+ screen.queryByTitle("This node is in a locked group and will be skipped during execution")
+ ).not.toBeInTheDocument();
+ });
+
+ it("should render lock badge when node is in a locked group", () => {
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ currentNodeId: null,
+ groups: { "group-1": { id: "group-1", locked: true } },
+ nodes: [{ id: "test-node-1", groupId: "group-1" }],
+ };
+ return selector(state);
+ });
+
+ render(
+
+
+
+ );
+
+ expect(
+ screen.getByTitle("This node is in a locked group and will be skipped during execution")
+ ).toBeInTheDocument();
+ });
+
+ it("should not render lock badge when node has no groupId", () => {
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ currentNodeId: null,
+ groups: { "group-1": { id: "group-1", locked: true } },
+ nodes: [{ id: "test-node-1" }], // No groupId
+ };
+ return selector(state);
+ });
+
+ render(
+
+
+
+ );
+
+ expect(
+ screen.queryByTitle("This node is in a locked group and will be skipped during execution")
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Visual States", () => {
+ it("should apply selected styling when selected is true", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const nodeDiv = container.querySelector(".ring-2.ring-blue-500\\/40");
+ expect(nodeDiv).toBeInTheDocument();
+ });
+
+ it("should apply executing styling when isExecuting is true", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const nodeDiv = container.querySelector(".border-blue-500.ring-1");
+ expect(nodeDiv).toBeInTheDocument();
+ });
+
+ it("should apply executing styling when currentNodeId matches", () => {
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ currentNodeId: "test-node-1",
+ groups: {},
+ nodes: [],
+ };
+ return selector(state);
+ });
+
+ const { container } = render(
+
+
+
+ );
+
+ const nodeDiv = container.querySelector(".border-blue-500.ring-1");
+ expect(nodeDiv).toBeInTheDocument();
+ });
+
+ it("should apply error styling when hasError is true", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const nodeDiv = container.querySelector(".border-red-500");
+ expect(nodeDiv).toBeInTheDocument();
+ });
+ });
+
+ describe("Node Resizer", () => {
+ it("should make resizer visible when selected", () => {
+ const { container } = render(
+
+
+
+ );
+
+ // NodeResizer component renders with isVisible prop
+ // We can check that the component was rendered by looking for resizer handles
+ const resizer = container.querySelector(".react-flow__resize-control");
+ // NodeResizer is visible when selected, component structure varies
+ expect(container.firstChild).toBeTruthy();
+ });
+
+ it("should use custom minWidth and minHeight", () => {
+ // This test verifies the props are passed, but the actual resizer behavior
+ // is managed by React Flow's NodeResizer component
+ render(
+
+
+
+ );
+
+ // Component renders without error with custom dimensions
+ expect(screen.getByText("Test Node")).toBeInTheDocument();
+ });
+ });
+});