From 80a535ee8d3206ead179e8d93a59414c8c5e65a0 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 6 Aug 2025 21:03:36 -0700 Subject: [PATCH] Add tests --- .../__tests__/DynamicJsonForm.test.tsx | 189 +++++++++++++++++- .../components/__tests__/ToolsTab.test.tsx | 167 ++++++++++++++++ 2 files changed, 355 insertions(+), 1 deletion(-) diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index 22813c9b..e1ad2c4f 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -1,7 +1,8 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import { describe, it, expect, jest } from "@jest/globals"; -import DynamicJsonForm from "../DynamicJsonForm"; +import { useRef } from "react"; +import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm"; import type { JsonSchemaType } from "@/utils/jsonUtils"; describe("DynamicJsonForm String Fields", () => { @@ -651,3 +652,189 @@ describe("DynamicJsonForm Copy JSON Functionality", () => { }); }); }); + +describe("DynamicJsonForm Validation Functionality", () => { + const renderFormWithRef = (props = {}) => { + const TestComponent = () => { + const formRef = useRef(null); + const defaultProps = { + schema: { + type: "object", + properties: { + nested: { oneOf: [{ type: "string" }, { type: "integer" }] }, + }, + } as unknown as JsonSchemaType, + value: { nested: "test value" }, + onChange: jest.fn(), + ref: formRef, + }; + + return ( +
+ + +
+ ); + }; + + return render(); + }; + + describe("validateJson method", () => { + it("should return valid for form mode", () => { + const simpleSchema = { + type: "string" as const, + description: "Test string field", + }; + + const TestComponent = () => { + const formRef = useRef(null); + + return ( +
+ + +
+ ); + }; + + render(); + + const validateButton = screen.getByTestId("validate-button"); + fireEvent.click(validateButton); + + expect(validateButton.getAttribute('data-validation-valid')).toBe('true'); + expect(validateButton.getAttribute('data-validation-error')).toBe(''); + }); + + it("should return valid for valid JSON in JSON mode", () => { + renderFormWithRef(); + + const validateButton = screen.getByTestId("validate-button"); + fireEvent.click(validateButton); + + expect(validateButton.getAttribute('data-validation-valid')).toBe('true'); + expect(validateButton.getAttribute('data-validation-error')).toBe(''); + }); + + it("should return invalid for malformed JSON in JSON mode", async () => { + renderFormWithRef(); + + // Enter invalid JSON + const textarea = screen.getByRole("textbox"); + fireEvent.change(textarea, { target: { value: '{ "invalid": json }' } }); + + // Wait a bit for any debounced updates + await waitFor(() => { + const validateButton = screen.getByTestId("validate-button"); + fireEvent.click(validateButton); + + expect(validateButton.getAttribute('data-validation-valid')).toBe('false'); + expect(validateButton.getAttribute('data-validation-error')).toContain('JSON'); + }); + }); + + it("should return valid for empty JSON in JSON mode", () => { + renderFormWithRef(); + + // Clear the textarea + const textarea = screen.getByRole("textbox"); + fireEvent.change(textarea, { target: { value: '' } }); + + const validateButton = screen.getByTestId("validate-button"); + fireEvent.click(validateButton); + + expect(validateButton.getAttribute('data-validation-valid')).toBe('true'); + expect(validateButton.getAttribute('data-validation-error')).toBe(''); + }); + + it("should set error state when validation fails", async () => { + renderFormWithRef(); + + // Enter invalid JSON + const textarea = screen.getByRole("textbox"); + fireEvent.change(textarea, { target: { value: '{ "trailing": "comma", }' } }); + + // Trigger validation + const validateButton = screen.getByTestId("validate-button"); + fireEvent.click(validateButton); + + // Check that validation result shows error + expect(validateButton.getAttribute('data-validation-valid')).toBe('false'); + expect(validateButton.getAttribute('data-validation-error')).toContain('JSON'); + }); + }); + + describe("forwardRef functionality", () => { + it("should expose validateJson method through ref", () => { + const TestComponent = () => { + const formRef = useRef(null); + + return ( +
+ + +
+ ); + }; + + render(); + + const testButton = screen.getByTestId("ref-test-button"); + fireEvent.click(testButton); + + expect(testButton.getAttribute('data-has-validate-method')).toBe('true'); + }); + }); +}); diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index bc75401d..a9e7b5fb 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -637,4 +637,171 @@ describe("ToolsTab", () => { expect(screen.getByText(/version/i)).toBeInTheDocument(); }); }); + + describe("JSON Validation Integration", () => { + const toolWithJsonParams: Tool = { + name: "jsonTool", + description: "Tool with JSON parameters", + inputSchema: { + type: "object" as const, + properties: { + config: { + type: "object" as const, + // No properties defined - this will force JSON mode + }, + data: { + type: "array" as const, + // No items defined - this will force JSON mode + }, + }, + }, + }; + + it("should prevent tool execution when JSON validation fails", async () => { + const mockCallTool = jest.fn(); + renderToolsTab({ + tools: [toolWithJsonParams], + selectedTool: toolWithJsonParams, + callTool: mockCallTool, + }); + + // Find JSON editor textareas (there should be at least 1 for JSON parameters) + const textareas = screen.getAllByRole("textbox"); + expect(textareas.length).toBeGreaterThanOrEqual(1); + + // Enter invalid JSON in the first textarea + const configTextarea = textareas[0]; + fireEvent.change(configTextarea, { + target: { value: '{ "invalid": json }' } + }); + + // Try to run the tool + const runButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(runButton); + }); + + // Tool should not have been called due to validation failure + expect(mockCallTool).not.toHaveBeenCalled(); + }); + + it("should allow tool execution when JSON validation passes", async () => { + const mockCallTool = jest.fn(); + renderToolsTab({ + tools: [toolWithJsonParams], + selectedTool: toolWithJsonParams, + callTool: mockCallTool, + }); + + // Find JSON editor textareas + const textareas = screen.getAllByRole("textbox"); + + // Enter valid JSON in the first textarea + fireEvent.change(textareas[0], { + target: { value: '{ "config": { "setting": "value" }, "data": ["item1", "item2"] }' } + }); + + // Wait for debounced updates + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 350)); + }); + + // Try to run the tool + const runButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(runButton); + }); + + // Tool should have been called successfully + expect(mockCallTool).toHaveBeenCalled(); + }); + + it("should handle mixed valid and invalid JSON parameters", async () => { + const mockCallTool = jest.fn(); + renderToolsTab({ + tools: [toolWithJsonParams], + selectedTool: toolWithJsonParams, + callTool: mockCallTool, + }); + + const textareas = screen.getAllByRole("textbox"); + + // Enter invalid JSON that contains both valid and invalid parts + fireEvent.change(textareas[0], { + target: { value: '{ "config": { "setting": "value" }, "data": ["unclosed array" }' } + }); + + // Try to run the tool + const runButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(runButton); + }); + + // Tool should not have been called due to validation failure + expect(mockCallTool).not.toHaveBeenCalled(); + }); + + it("should work with tools that have no JSON parameters", async () => { + const mockCallTool = jest.fn(); + const simpleToolWithStringParam: Tool = { + name: "simpleTool", + description: "Tool with simple parameters", + inputSchema: { + type: "object" as const, + properties: { + message: { type: "string" as const }, + count: { type: "number" as const }, + }, + }, + }; + + renderToolsTab({ + tools: [simpleToolWithStringParam], + selectedTool: simpleToolWithStringParam, + callTool: mockCallTool, + }); + + // Fill in the simple parameters + const messageInput = screen.getByRole("textbox"); + const countInput = screen.getByRole("spinbutton"); + + fireEvent.change(messageInput, { target: { value: "test message" } }); + fireEvent.change(countInput, { target: { value: "5" } }); + + // Run the tool + const runButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(runButton); + }); + + // Tool should have been called successfully (no JSON validation needed) + expect(mockCallTool).toHaveBeenCalledWith(simpleToolWithStringParam.name, { + message: "test message", + count: 5, + }); + }); + + it("should handle empty JSON parameters correctly", async () => { + const mockCallTool = jest.fn(); + renderToolsTab({ + tools: [toolWithJsonParams], + selectedTool: toolWithJsonParams, + callTool: mockCallTool, + }); + + const textareas = screen.getAllByRole("textbox"); + + // Clear the textarea (empty JSON should be valid) + fireEvent.change(textareas[0], { target: { value: '' } }); + + // Try to run the tool + const runButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(runButton); + }); + + // Tool should have been called (empty JSON is considered valid) + expect(mockCallTool).toHaveBeenCalled(); + }); + }); });