From 3f0c45e10dca7ff3a3bcd35c218a3f009830bd91 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 3 Dec 2025 09:58:18 -0500 Subject: [PATCH] update DynamicJsonForm tests; add enum coverage and JSON-mode assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align tests with new form-first rendering and UI behavior: - Labels prefer schema `title` over property key. - Descriptions shown for boolean checkboxes and select fields. - Explicit mode switching between Form/JSON when needed. - Add comprehensive coverage for enum schema variants: - Titled single-select via `oneOf`/`anyOf` with `{ const, title }`. - Untitled single-select via `enum` (with legacy `enumNames` labels). - Untitled multi-select via `items.enum` with `minItems`/`maxItems` helper text. - Titled multi-select via `items.anyOf`/`oneOf` with `{ const, title }`. - Adjust complex/JSON-mode tests to click “Switch to JSON” before asserting textarea and clipboard interactions. - Fix debounced JSON update expectations to assert parsed objects rather than raw text. Files updated: - `client/src/components/__tests__/DynamicJsonForm.test.tsx` - `client/src/components/__tests__/DynamicJsonForm.array.test.tsx` --- .../__tests__/DynamicJsonForm.array.test.tsx | 82 +++++++++++++- .../__tests__/DynamicJsonForm.test.tsx | 105 ++++++++++++++++-- 2 files changed, 178 insertions(+), 9 deletions(-) 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, {