diff --git a/src/components/__tests__/LLMGenerateNode.test.tsx b/src/components/__tests__/LLMGenerateNode.test.tsx
new file mode 100644
index 00000000..cbd478a4
--- /dev/null
+++ b/src/components/__tests__/LLMGenerateNode.test.tsx
@@ -0,0 +1,403 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { LLMGenerateNode } from "@/components/nodes/LLMGenerateNode";
+import { ReactFlowProvider } from "@xyflow/react";
+import { LLMGenerateNodeData } from "@/types";
+
+// Mock the workflow store
+const mockUpdateNodeData = vi.fn();
+const mockRegenerateNode = vi.fn();
+const mockUseWorkflowStore = vi.fn();
+
+vi.mock("@/store/workflowStore", () => ({
+ useWorkflowStore: (selector?: (state: unknown) => unknown) => {
+ if (selector) {
+ return mockUseWorkflowStore(selector);
+ }
+ return mockUseWorkflowStore((s: unknown) => s);
+ },
+}));
+
+// Wrapper component for React Flow context
+function TestWrapper({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+
+describe("LLMGenerateNode", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Default mock implementation
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ updateNodeData: mockUpdateNodeData,
+ regenerateNode: mockRegenerateNode,
+ isRunning: false,
+ currentNodeId: null,
+ groups: {},
+ nodes: [],
+ };
+ return selector(state);
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ const createNodeData = (overrides: Partial = {}): LLMGenerateNodeData => ({
+ inputPrompt: null,
+ inputImages: [],
+ outputText: null,
+ provider: "google",
+ model: "gemini-3-flash-preview",
+ temperature: 1.0,
+ maxTokens: 2048,
+ status: "idle",
+ error: null,
+ ...overrides,
+ });
+
+ const createNodeProps = (data: Partial = {}) => ({
+ id: "test-llm-1",
+ type: "llmGenerate" as const,
+ data: createNodeData(data),
+ selected: false,
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render with title 'LLM Generate'", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("LLM Generate")).toBeInTheDocument();
+ });
+
+ it("should render text input handle on left", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const textHandle = container.querySelector('[data-handletype="text"][class*="target"]');
+ expect(textHandle).toBeInTheDocument();
+ });
+
+ it("should render image input handle on left", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const imageHandle = container.querySelector('[data-handletype="image"][class*="target"]');
+ expect(imageHandle).toBeInTheDocument();
+ });
+
+ it("should render text output handle on right", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const outputHandle = container.querySelector('[data-handletype="text"][class*="source"]');
+ expect(outputHandle).toBeInTheDocument();
+ });
+ });
+
+ describe("Provider Selector", () => {
+ it("should render provider selector with Google selected by default", () => {
+ render(
+
+
+
+ );
+
+ const providerSelect = screen.getByDisplayValue("Google");
+ expect(providerSelect).toBeInTheDocument();
+ });
+
+ it("should show Google and OpenAI as provider options", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("option", { name: "Google" })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: "OpenAI" })).toBeInTheDocument();
+ });
+
+ it("should call updateNodeData when provider is changed", () => {
+ render(
+
+
+
+ );
+
+ const providerSelect = screen.getByDisplayValue("Google");
+ fireEvent.change(providerSelect, { target: { value: "openai" } });
+
+ expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", {
+ provider: "openai",
+ model: "gpt-4.1-mini",
+ });
+ });
+ });
+
+ describe("Model Selector", () => {
+ it("should show Google models when Google provider is selected", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("option", { name: "Gemini 3 Flash" })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: "Gemini 2.5 Flash" })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: "Gemini 3.0 Pro" })).toBeInTheDocument();
+ });
+
+ it("should show OpenAI models when OpenAI provider is selected", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("option", { name: "GPT-4.1 Mini" })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: "GPT-4.1 Nano" })).toBeInTheDocument();
+ });
+
+ it("should call updateNodeData when model is changed", () => {
+ render(
+
+
+
+ );
+
+ const modelSelect = screen.getByDisplayValue("Gemini 3 Flash");
+ fireEvent.change(modelSelect, { target: { value: "gemini-2.5-flash" } });
+
+ expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", {
+ model: "gemini-2.5-flash",
+ });
+ });
+ });
+
+ describe("Temperature Slider", () => {
+ it("should render temperature slider with current value", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Temp: 0.7")).toBeInTheDocument();
+ });
+
+ it("should call updateNodeData when temperature is changed", () => {
+ render(
+
+
+
+ );
+
+ const slider = screen.getByRole("slider");
+ fireEvent.change(slider, { target: { value: "1.5" } });
+
+ expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", {
+ temperature: 1.5,
+ });
+ });
+ });
+
+ describe("Idle State", () => {
+ it("should show 'Run to generate' message when idle and no output", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Run to generate")).toBeInTheDocument();
+ });
+ });
+
+ describe("Loading State", () => {
+ it("should show loading spinner when status is loading", () => {
+ const { container } = render(
+
+
+
+ );
+
+ const spinner = container.querySelector(".animate-spin");
+ expect(spinner).toBeInTheDocument();
+ });
+ });
+
+ describe("Error State", () => {
+ it("should show error message when status is error", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("API rate limit exceeded")).toBeInTheDocument();
+ });
+
+ it("should show 'Failed' when error message is null", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Failed")).toBeInTheDocument();
+ });
+ });
+
+ describe("Output Text Display", () => {
+ it("should display output text when data.outputText exists", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Generated response text")).toBeInTheDocument();
+ });
+
+ it("should render regenerate button when output exists", () => {
+ render(
+
+
+
+ );
+
+ const regenerateButton = screen.getByTitle("Regenerate");
+ expect(regenerateButton).toBeInTheDocument();
+ });
+
+ it("should call regenerateNode when regenerate button is clicked", () => {
+ render(
+
+
+
+ );
+
+ const regenerateButton = screen.getByTitle("Regenerate");
+ fireEvent.click(regenerateButton);
+
+ expect(mockRegenerateNode).toHaveBeenCalledWith("test-llm-1");
+ });
+
+ it("should disable regenerate button when workflow is running", () => {
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ const state = {
+ updateNodeData: mockUpdateNodeData,
+ regenerateNode: mockRegenerateNode,
+ isRunning: true,
+ currentNodeId: null,
+ groups: {},
+ nodes: [],
+ };
+ return selector(state);
+ });
+
+ render(
+
+
+
+ );
+
+ const regenerateButton = screen.getByTitle("Regenerate");
+ expect(regenerateButton).toBeDisabled();
+ });
+ });
+
+ describe("Clear Output Button", () => {
+ it("should render clear output button when output exists", () => {
+ render(
+
+
+
+ );
+
+ const clearButton = screen.getByTitle("Clear output");
+ expect(clearButton).toBeInTheDocument();
+ });
+
+ it("should call updateNodeData to clear output when clear button is clicked", () => {
+ render(
+
+
+
+ );
+
+ const clearButton = screen.getByTitle("Clear output");
+ fireEvent.click(clearButton);
+
+ expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", {
+ outputText: null,
+ status: "idle",
+ error: null,
+ });
+ });
+ });
+
+ describe("Custom Title and Comment", () => {
+ it("should display custom title when provided", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText(/My LLM/)).toBeInTheDocument();
+ });
+
+ it("should call updateNodeData when custom title is changed", () => {
+ render(
+
+
+
+ );
+
+ // Click on title to edit
+ const title = screen.getByText("LLM Generate");
+ fireEvent.click(title);
+
+ const input = screen.getByPlaceholderText("Custom title...");
+ fireEvent.change(input, { target: { value: "Custom LLM Title" } });
+ fireEvent.keyDown(input, { key: "Enter" });
+
+ expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", { customTitle: "Custom LLM Title" });
+ });
+ });
+
+ describe("Model Fallback", () => {
+ it("should fall back to first available model when current model is invalid for provider", () => {
+ // When provider is google but model is an OpenAI model, should show first Google model
+ render(
+
+
+
+ );
+
+ // Should show first Google model in select
+ const modelSelect = screen.getByDisplayValue("Gemini 3 Flash");
+ expect(modelSelect).toBeInTheDocument();
+ });
+ });
+});