diff --git a/client/src/components/__tests__/DynamicJsonForm.array.test.tsx b/client/src/components/__tests__/DynamicJsonForm.array.test.tsx
index 00a324bf..39dfb2b9 100644
--- a/client/src/components/__tests__/DynamicJsonForm.array.test.tsx
+++ b/client/src/components/__tests__/DynamicJsonForm.array.test.tsx
@@ -1,4 +1,5 @@
import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
import { describe, it, expect, jest } from "@jest/globals";
import DynamicJsonForm from "../DynamicJsonForm";
import type { JsonSchemaType } from "@/utils/jsonUtils";
@@ -104,7 +105,11 @@ describe("DynamicJsonForm Array Fields", () => {
it("should render JSON editor for complex arrays", () => {
renderComplexArrayForm();
- // Should render as JSON editor (textarea)
+ // Initially renders form view with Switch to JSON button; switch to JSON to see textarea
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ expect(switchBtn).toBeInTheDocument();
+ fireEvent.click(switchBtn);
+
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveProperty("type", "textarea");
@@ -173,6 +178,81 @@ describe("DynamicJsonForm Array Fields", () => {
});
describe("Array with Different Item Types", () => {
+ it("should render multi-select for untitled items.enum with helper text", () => {
+ const schema: JsonSchemaType = {
+ type: "array",
+ title: "Colors",
+ description: "Pick colors",
+ minItems: 1,
+ maxItems: 3,
+ items: { type: "string", enum: ["red", "green", "blue"] },
+ } as JsonSchemaType;
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+
+ // Description visible
+ expect(screen.getByText("Pick colors")).toBeInTheDocument();
+ // Multi-select present
+ const listbox = screen.getByRole("listbox");
+ expect(listbox).toHaveAttribute("multiple");
+
+ // Helper text shows min/max
+ expect(screen.getByText(/Select at least 1/i)).toBeInTheDocument();
+ expect(screen.getByText(/Select at most 3/i)).toBeInTheDocument();
+
+ // Select another option by toggling option.selected
+ const colorOptions = screen.getAllByRole("option");
+ // options: red, green, blue
+ colorOptions[0].selected = true; // red
+ colorOptions[2].selected = true; // blue
+ fireEvent.change(listbox);
+ expect(onChange).toHaveBeenCalledWith(["red", "blue"]);
+ });
+
+ it("should render titled multi-select for items.anyOf with const/title", () => {
+ const schema: JsonSchemaType = {
+ type: "array",
+ description: "Pick fish",
+ items: {
+ anyOf: [
+ { const: "fish-1", title: "Tuna" },
+ { const: "fish-2", title: "Salmon" },
+ { const: "fish-3", title: "Trout" },
+ ],
+ } as unknown as JsonSchemaType,
+ } as unknown as JsonSchemaType;
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+
+ // Description visible
+ expect(screen.getByText("Pick fish")).toBeInTheDocument();
+
+ const listbox = screen.getByRole("listbox");
+ expect(listbox).toHaveAttribute("multiple");
+
+ // Ensure options have titles as labels
+ const options = screen.getAllByRole("option");
+ expect(options[0]).toHaveProperty("textContent", "Tuna");
+ expect(options[1]).toHaveProperty("textContent", "Salmon");
+ expect(options[2]).toHaveProperty("textContent", "Trout");
+
+ // Select fish-2 and fish-3
+ const fishOptions = screen.getAllByRole("option");
+ // options: Tuna (fish-1), Salmon (fish-2), Trout (fish-3)
+ fishOptions[0].selected = false; // deselect fish-1
+ fishOptions[1].selected = true; // fish-2
+ fishOptions[2].selected = true; // fish-3
+ fireEvent.change(listbox);
+ expect(onChange).toHaveBeenCalledWith(["fish-2", "fish-3"]);
+ });
it("should handle integer array items", () => {
const schema = {
type: "array" as const,
diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx
index 770c7e66..a9f17d55 100644
--- a/client/src/components/__tests__/DynamicJsonForm.test.tsx
+++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx
@@ -202,6 +202,55 @@ describe("DynamicJsonForm String Fields", () => {
fireEvent.change(select, { target: { value: "amber" } });
expect(onChange).toHaveBeenCalledWith("amber");
});
+
+ it("should render anyOf with const/title for labeled options and show description", () => {
+ const onChange = jest.fn();
+ const schema: JsonSchemaType = {
+ type: "string",
+ title: "Heroes",
+ description: "Choose a hero",
+ anyOf: [
+ { const: "hero-1", title: "Superman" },
+ { const: "hero-2", title: "Batman" },
+ ],
+ };
+ render();
+
+ // Description should be visible above the select
+ expect(screen.getByText("Choose a hero")).toBeInTheDocument();
+ const select = screen.getByRole("combobox");
+ const options = screen.getAllByRole("option");
+ expect(options).toHaveLength(3);
+ expect(options[1]).toHaveProperty("textContent", "Superman");
+ expect(options[2]).toHaveProperty("textContent", "Batman");
+
+ fireEvent.change(select, { target: { value: "hero-2" } });
+ expect(onChange).toHaveBeenCalledWith("hero-2");
+ });
+
+ it("should render legacy enum with enumNames as labels", () => {
+ const onChange = jest.fn();
+ const schema: JsonSchemaType = {
+ type: "string",
+ title: "Pets",
+ description: "Choose a pet",
+ enum: ["pet-1", "pet-2", "pet-3"],
+ enumNames: ["Cat", "Dog", "Bird"],
+ } as unknown as JsonSchemaType; // enumNames is legacy extension
+
+ render();
+
+ // Description should be visible above the select
+ expect(screen.getByText("Choose a pet")).toBeInTheDocument();
+ const options = screen.getAllByRole("option");
+ expect(options[1]).toHaveProperty("textContent", "Cat");
+ expect(options[2]).toHaveProperty("textContent", "Dog");
+ expect(options[3]).toHaveProperty("textContent", "Bird");
+
+ const select = screen.getByRole("combobox");
+ fireEvent.change(select, { target: { value: "pet-2" } });
+ expect(onChange).toHaveBeenCalledWith("pet-2");
+ });
});
describe("Validation Attributes", () => {
@@ -535,8 +584,8 @@ describe("DynamicJsonForm Object Fields", () => {
,
);
- const nameLabel = screen.getByText("name");
- const optionalLabel = screen.getByText("optional");
+ const nameLabel = screen.getByText("Name");
+ const optionalLabel = screen.getByText("Optional");
const nameInput = nameLabel.closest("div")?.querySelector("input");
const optionalInput = optionalLabel
@@ -567,30 +616,46 @@ describe("DynamicJsonForm Complex Fields", () => {
};
describe("Basic Operations", () => {
- it("should render textbox and autoformat button, but no switch-to-form button", () => {
+ it("should allow switching to JSON mode and show copy/format buttons", () => {
renderForm();
+
+ // Initially renders as a form with a Switch to JSON button
+ const switchToJson = screen.getByRole("button", {
+ name: /switch to json/i,
+ });
+ expect(switchToJson).toBeInTheDocument();
+
+ // Switch to JSON mode
+ fireEvent.click(switchToJson);
+
+ // Now a textarea and JSON helpers should be visible
const input = screen.getByRole("textbox");
expect(input).toHaveProperty("type", "textarea");
- const buttons = screen.getAllByRole("button");
- expect(buttons).toHaveLength(2); // Copy JSON + Format JSON
const copyButton = screen.getByRole("button", { name: /copy json/i });
const formatButton = screen.getByRole("button", { name: /format json/i });
expect(copyButton).toBeTruthy();
expect(formatButton).toBeTruthy();
+ // And a Switch to Form button should appear
+ expect(
+ screen.getByRole("button", { name: /switch to form/i }),
+ ).toBeInTheDocument();
});
- it("should pass changed values to onChange", () => {
+ it("should pass changed values to onChange in JSON mode", () => {
const onChange = jest.fn();
renderForm({ onChange });
+ // Switch to JSON mode first
+ fireEvent.click(screen.getByRole("button", { name: /switch to json/i }));
+
const input = screen.getByRole("textbox");
fireEvent.change(input, {
target: { value: `{ "nested": "i am string" }` },
});
// The onChange handler is debounced when using the JSON view, so we need to wait a little bit
- waitFor(() => {
- expect(onChange).toHaveBeenCalledWith(`{ "nested": "i am string" }`);
+ return waitFor(() => {
+ expect(onChange).toHaveBeenCalledWith({ nested: "i am string" });
});
});
});
@@ -626,6 +691,10 @@ describe("DynamicJsonForm Copy JSON Functionality", () => {
it("should render Copy JSON button when in JSON mode", () => {
renderFormInJsonMode();
+ // Switch to JSON mode to reveal copy/format buttons
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
expect(copyButton).toBeTruthy();
});
@@ -653,6 +722,10 @@ describe("DynamicJsonForm Copy JSON Functionality", () => {
renderFormInJsonMode({ value: testValue });
+ // Switch to JSON mode first
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
fireEvent.click(copyButton);
@@ -768,6 +841,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
it("should return valid for valid JSON in JSON mode", () => {
renderFormWithRef();
+ // Switch to JSON mode to enable textarea editing/validation
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
const validateButton = screen.getByTestId("validate-button");
fireEvent.click(validateButton);
@@ -778,6 +855,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
it("should return invalid for malformed JSON in JSON mode", async () => {
renderFormWithRef();
+ // Switch to JSON mode first
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
// Enter invalid JSON
const textarea = screen.getByRole("textbox");
fireEvent.change(textarea, { target: { value: '{ "invalid": json }' } });
@@ -799,6 +880,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
it("should return valid for empty JSON in JSON mode", () => {
renderFormWithRef();
+ // Switch to JSON mode first
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
// Clear the textarea
const textarea = screen.getByRole("textbox");
fireEvent.change(textarea, { target: { value: "" } });
@@ -813,6 +898,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
it("should set error state when validation fails", async () => {
renderFormWithRef();
+ // Switch to JSON mode first
+ const switchBtn = screen.getByRole("button", { name: /switch to json/i });
+ fireEvent.click(switchBtn);
+
// Enter invalid JSON
const textarea = screen.getByRole("textbox");
fireEvent.change(textarea, {