Skip to content

Commit 80a535e

Browse files
committed
Add tests
1 parent 8ce1bbd commit 80a535e

2 files changed

Lines changed: 355 additions & 1 deletion

File tree

client/src/components/__tests__/DynamicJsonForm.test.tsx

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22
import "@testing-library/jest-dom";
33
import { describe, it, expect, jest } from "@jest/globals";
4-
import DynamicJsonForm from "../DynamicJsonForm";
4+
import { useRef } from "react";
5+
import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm";
56
import type { JsonSchemaType } from "@/utils/jsonUtils";
67

78
describe("DynamicJsonForm String Fields", () => {
@@ -651,3 +652,189 @@ describe("DynamicJsonForm Copy JSON Functionality", () => {
651652
});
652653
});
653654
});
655+
656+
describe("DynamicJsonForm Validation Functionality", () => {
657+
const renderFormWithRef = (props = {}) => {
658+
const TestComponent = () => {
659+
const formRef = useRef<DynamicJsonFormRef>(null);
660+
const defaultProps = {
661+
schema: {
662+
type: "object",
663+
properties: {
664+
nested: { oneOf: [{ type: "string" }, { type: "integer" }] },
665+
},
666+
} as unknown as JsonSchemaType,
667+
value: { nested: "test value" },
668+
onChange: jest.fn(),
669+
ref: formRef,
670+
};
671+
672+
return (
673+
<div>
674+
<DynamicJsonForm {...defaultProps} {...props} />
675+
<button
676+
onClick={() => {
677+
const result = formRef.current?.validateJson();
678+
// Add data attributes to make validation result testable
679+
const button = document.querySelector('[data-testid="validate-button"]') as HTMLElement;
680+
if (button && result) {
681+
button.setAttribute('data-validation-valid', result.isValid.toString());
682+
button.setAttribute('data-validation-error', result.error || '');
683+
}
684+
}}
685+
data-testid="validate-button"
686+
>
687+
Validate
688+
</button>
689+
</div>
690+
);
691+
};
692+
693+
return render(<TestComponent />);
694+
};
695+
696+
describe("validateJson method", () => {
697+
it("should return valid for form mode", () => {
698+
const simpleSchema = {
699+
type: "string" as const,
700+
description: "Test string field",
701+
};
702+
703+
const TestComponent = () => {
704+
const formRef = useRef<DynamicJsonFormRef>(null);
705+
706+
return (
707+
<div>
708+
<DynamicJsonForm
709+
ref={formRef}
710+
schema={simpleSchema}
711+
value="test"
712+
onChange={jest.fn()}
713+
/>
714+
<button
715+
onClick={() => {
716+
const result = formRef.current?.validateJson();
717+
const button = document.querySelector('[data-testid="validate-button"]') as HTMLElement;
718+
if (button && result) {
719+
button.setAttribute('data-validation-valid', result.isValid.toString());
720+
button.setAttribute('data-validation-error', result.error || '');
721+
}
722+
}}
723+
data-testid="validate-button"
724+
>
725+
Validate
726+
</button>
727+
</div>
728+
);
729+
};
730+
731+
render(<TestComponent />);
732+
733+
const validateButton = screen.getByTestId("validate-button");
734+
fireEvent.click(validateButton);
735+
736+
expect(validateButton.getAttribute('data-validation-valid')).toBe('true');
737+
expect(validateButton.getAttribute('data-validation-error')).toBe('');
738+
});
739+
740+
it("should return valid for valid JSON in JSON mode", () => {
741+
renderFormWithRef();
742+
743+
const validateButton = screen.getByTestId("validate-button");
744+
fireEvent.click(validateButton);
745+
746+
expect(validateButton.getAttribute('data-validation-valid')).toBe('true');
747+
expect(validateButton.getAttribute('data-validation-error')).toBe('');
748+
});
749+
750+
it("should return invalid for malformed JSON in JSON mode", async () => {
751+
renderFormWithRef();
752+
753+
// Enter invalid JSON
754+
const textarea = screen.getByRole("textbox");
755+
fireEvent.change(textarea, { target: { value: '{ "invalid": json }' } });
756+
757+
// Wait a bit for any debounced updates
758+
await waitFor(() => {
759+
const validateButton = screen.getByTestId("validate-button");
760+
fireEvent.click(validateButton);
761+
762+
expect(validateButton.getAttribute('data-validation-valid')).toBe('false');
763+
expect(validateButton.getAttribute('data-validation-error')).toContain('JSON');
764+
});
765+
});
766+
767+
it("should return valid for empty JSON in JSON mode", () => {
768+
renderFormWithRef();
769+
770+
// Clear the textarea
771+
const textarea = screen.getByRole("textbox");
772+
fireEvent.change(textarea, { target: { value: '' } });
773+
774+
const validateButton = screen.getByTestId("validate-button");
775+
fireEvent.click(validateButton);
776+
777+
expect(validateButton.getAttribute('data-validation-valid')).toBe('true');
778+
expect(validateButton.getAttribute('data-validation-error')).toBe('');
779+
});
780+
781+
it("should set error state when validation fails", async () => {
782+
renderFormWithRef();
783+
784+
// Enter invalid JSON
785+
const textarea = screen.getByRole("textbox");
786+
fireEvent.change(textarea, { target: { value: '{ "trailing": "comma", }' } });
787+
788+
// Trigger validation
789+
const validateButton = screen.getByTestId("validate-button");
790+
fireEvent.click(validateButton);
791+
792+
// Check that validation result shows error
793+
expect(validateButton.getAttribute('data-validation-valid')).toBe('false');
794+
expect(validateButton.getAttribute('data-validation-error')).toContain('JSON');
795+
});
796+
});
797+
798+
describe("forwardRef functionality", () => {
799+
it("should expose validateJson method through ref", () => {
800+
const TestComponent = () => {
801+
const formRef = useRef<DynamicJsonFormRef>(null);
802+
803+
return (
804+
<div>
805+
<DynamicJsonForm
806+
ref={formRef}
807+
schema={{
808+
type: "object",
809+
properties: {
810+
test: { type: "string" },
811+
},
812+
}}
813+
value={{ test: "value" }}
814+
onChange={jest.fn()}
815+
/>
816+
<button
817+
onClick={() => {
818+
const hasValidateMethod = typeof formRef.current?.validateJson === 'function';
819+
const button = document.querySelector('[data-testid="ref-test-button"]') as HTMLElement;
820+
if (button) {
821+
button.setAttribute('data-has-validate-method', hasValidateMethod.toString());
822+
}
823+
}}
824+
data-testid="ref-test-button"
825+
>
826+
Test Ref
827+
</button>
828+
</div>
829+
);
830+
};
831+
832+
render(<TestComponent />);
833+
834+
const testButton = screen.getByTestId("ref-test-button");
835+
fireEvent.click(testButton);
836+
837+
expect(testButton.getAttribute('data-has-validate-method')).toBe('true');
838+
});
839+
});
840+
});

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,4 +637,171 @@ describe("ToolsTab", () => {
637637
expect(screen.getByText(/version/i)).toBeInTheDocument();
638638
});
639639
});
640+
641+
describe("JSON Validation Integration", () => {
642+
const toolWithJsonParams: Tool = {
643+
name: "jsonTool",
644+
description: "Tool with JSON parameters",
645+
inputSchema: {
646+
type: "object" as const,
647+
properties: {
648+
config: {
649+
type: "object" as const,
650+
// No properties defined - this will force JSON mode
651+
},
652+
data: {
653+
type: "array" as const,
654+
// No items defined - this will force JSON mode
655+
},
656+
},
657+
},
658+
};
659+
660+
it("should prevent tool execution when JSON validation fails", async () => {
661+
const mockCallTool = jest.fn();
662+
renderToolsTab({
663+
tools: [toolWithJsonParams],
664+
selectedTool: toolWithJsonParams,
665+
callTool: mockCallTool,
666+
});
667+
668+
// Find JSON editor textareas (there should be at least 1 for JSON parameters)
669+
const textareas = screen.getAllByRole("textbox");
670+
expect(textareas.length).toBeGreaterThanOrEqual(1);
671+
672+
// Enter invalid JSON in the first textarea
673+
const configTextarea = textareas[0];
674+
fireEvent.change(configTextarea, {
675+
target: { value: '{ "invalid": json }' }
676+
});
677+
678+
// Try to run the tool
679+
const runButton = screen.getByRole("button", { name: /run tool/i });
680+
await act(async () => {
681+
fireEvent.click(runButton);
682+
});
683+
684+
// Tool should not have been called due to validation failure
685+
expect(mockCallTool).not.toHaveBeenCalled();
686+
});
687+
688+
it("should allow tool execution when JSON validation passes", async () => {
689+
const mockCallTool = jest.fn();
690+
renderToolsTab({
691+
tools: [toolWithJsonParams],
692+
selectedTool: toolWithJsonParams,
693+
callTool: mockCallTool,
694+
});
695+
696+
// Find JSON editor textareas
697+
const textareas = screen.getAllByRole("textbox");
698+
699+
// Enter valid JSON in the first textarea
700+
fireEvent.change(textareas[0], {
701+
target: { value: '{ "config": { "setting": "value" }, "data": ["item1", "item2"] }' }
702+
});
703+
704+
// Wait for debounced updates
705+
await act(async () => {
706+
await new Promise(resolve => setTimeout(resolve, 350));
707+
});
708+
709+
// Try to run the tool
710+
const runButton = screen.getByRole("button", { name: /run tool/i });
711+
await act(async () => {
712+
fireEvent.click(runButton);
713+
});
714+
715+
// Tool should have been called successfully
716+
expect(mockCallTool).toHaveBeenCalled();
717+
});
718+
719+
it("should handle mixed valid and invalid JSON parameters", async () => {
720+
const mockCallTool = jest.fn();
721+
renderToolsTab({
722+
tools: [toolWithJsonParams],
723+
selectedTool: toolWithJsonParams,
724+
callTool: mockCallTool,
725+
});
726+
727+
const textareas = screen.getAllByRole("textbox");
728+
729+
// Enter invalid JSON that contains both valid and invalid parts
730+
fireEvent.change(textareas[0], {
731+
target: { value: '{ "config": { "setting": "value" }, "data": ["unclosed array" }' }
732+
});
733+
734+
// Try to run the tool
735+
const runButton = screen.getByRole("button", { name: /run tool/i });
736+
await act(async () => {
737+
fireEvent.click(runButton);
738+
});
739+
740+
// Tool should not have been called due to validation failure
741+
expect(mockCallTool).not.toHaveBeenCalled();
742+
});
743+
744+
it("should work with tools that have no JSON parameters", async () => {
745+
const mockCallTool = jest.fn();
746+
const simpleToolWithStringParam: Tool = {
747+
name: "simpleTool",
748+
description: "Tool with simple parameters",
749+
inputSchema: {
750+
type: "object" as const,
751+
properties: {
752+
message: { type: "string" as const },
753+
count: { type: "number" as const },
754+
},
755+
},
756+
};
757+
758+
renderToolsTab({
759+
tools: [simpleToolWithStringParam],
760+
selectedTool: simpleToolWithStringParam,
761+
callTool: mockCallTool,
762+
});
763+
764+
// Fill in the simple parameters
765+
const messageInput = screen.getByRole("textbox");
766+
const countInput = screen.getByRole("spinbutton");
767+
768+
fireEvent.change(messageInput, { target: { value: "test message" } });
769+
fireEvent.change(countInput, { target: { value: "5" } });
770+
771+
// Run the tool
772+
const runButton = screen.getByRole("button", { name: /run tool/i });
773+
await act(async () => {
774+
fireEvent.click(runButton);
775+
});
776+
777+
// Tool should have been called successfully (no JSON validation needed)
778+
expect(mockCallTool).toHaveBeenCalledWith(simpleToolWithStringParam.name, {
779+
message: "test message",
780+
count: 5,
781+
});
782+
});
783+
784+
it("should handle empty JSON parameters correctly", async () => {
785+
const mockCallTool = jest.fn();
786+
renderToolsTab({
787+
tools: [toolWithJsonParams],
788+
selectedTool: toolWithJsonParams,
789+
callTool: mockCallTool,
790+
});
791+
792+
const textareas = screen.getAllByRole("textbox");
793+
794+
// Clear the textarea (empty JSON should be valid)
795+
fireEvent.change(textareas[0], { target: { value: '' } });
796+
797+
// Try to run the tool
798+
const runButton = screen.getByRole("button", { name: /run tool/i });
799+
await act(async () => {
800+
fireEvent.click(runButton);
801+
});
802+
803+
// Tool should have been called (empty JSON is considered valid)
804+
expect(mockCallTool).toHaveBeenCalled();
805+
});
806+
});
640807
});

0 commit comments

Comments
 (0)