Skip to content

Commit 3f0c45e

Browse files
committed
update DynamicJsonForm tests; add enum coverage and JSON-mode assertions
- 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`
1 parent fd99d87 commit 3f0c45e

File tree

2 files changed

+178
-9
lines changed

2 files changed

+178
-9
lines changed

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen, fireEvent } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
23
import { describe, it, expect, jest } from "@jest/globals";
34
import DynamicJsonForm from "../DynamicJsonForm";
45
import type { JsonSchemaType } from "@/utils/jsonUtils";
@@ -104,7 +105,11 @@ describe("DynamicJsonForm Array Fields", () => {
104105
it("should render JSON editor for complex arrays", () => {
105106
renderComplexArrayForm();
106107

107-
// Should render as JSON editor (textarea)
108+
// Initially renders form view with Switch to JSON button; switch to JSON to see textarea
109+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
110+
expect(switchBtn).toBeInTheDocument();
111+
fireEvent.click(switchBtn);
112+
108113
const textarea = screen.getByRole("textbox");
109114
expect(textarea).toHaveProperty("type", "textarea");
110115

@@ -173,6 +178,81 @@ describe("DynamicJsonForm Array Fields", () => {
173178
});
174179

175180
describe("Array with Different Item Types", () => {
181+
it("should render multi-select for untitled items.enum with helper text", () => {
182+
const schema: JsonSchemaType = {
183+
type: "array",
184+
title: "Colors",
185+
description: "Pick colors",
186+
minItems: 1,
187+
maxItems: 3,
188+
items: { type: "string", enum: ["red", "green", "blue"] },
189+
} as JsonSchemaType;
190+
const onChange = jest.fn();
191+
render(
192+
<DynamicJsonForm schema={schema} value={["red"]} onChange={onChange} />,
193+
);
194+
195+
// Description visible
196+
expect(screen.getByText("Pick colors")).toBeInTheDocument();
197+
// Multi-select present
198+
const listbox = screen.getByRole("listbox");
199+
expect(listbox).toHaveAttribute("multiple");
200+
201+
// Helper text shows min/max
202+
expect(screen.getByText(/Select at least 1/i)).toBeInTheDocument();
203+
expect(screen.getByText(/Select at most 3/i)).toBeInTheDocument();
204+
205+
// Select another option by toggling option.selected
206+
const colorOptions = screen.getAllByRole("option");
207+
// options: red, green, blue
208+
colorOptions[0].selected = true; // red
209+
colorOptions[2].selected = true; // blue
210+
fireEvent.change(listbox);
211+
expect(onChange).toHaveBeenCalledWith(["red", "blue"]);
212+
});
213+
214+
it("should render titled multi-select for items.anyOf with const/title", () => {
215+
const schema: JsonSchemaType = {
216+
type: "array",
217+
description: "Pick fish",
218+
items: {
219+
anyOf: [
220+
{ const: "fish-1", title: "Tuna" },
221+
{ const: "fish-2", title: "Salmon" },
222+
{ const: "fish-3", title: "Trout" },
223+
],
224+
} as unknown as JsonSchemaType,
225+
} as unknown as JsonSchemaType;
226+
const onChange = jest.fn();
227+
render(
228+
<DynamicJsonForm
229+
schema={schema}
230+
value={["fish-1"]}
231+
onChange={onChange}
232+
/>,
233+
);
234+
235+
// Description visible
236+
expect(screen.getByText("Pick fish")).toBeInTheDocument();
237+
238+
const listbox = screen.getByRole("listbox");
239+
expect(listbox).toHaveAttribute("multiple");
240+
241+
// Ensure options have titles as labels
242+
const options = screen.getAllByRole("option");
243+
expect(options[0]).toHaveProperty("textContent", "Tuna");
244+
expect(options[1]).toHaveProperty("textContent", "Salmon");
245+
expect(options[2]).toHaveProperty("textContent", "Trout");
246+
247+
// Select fish-2 and fish-3
248+
const fishOptions = screen.getAllByRole("option");
249+
// options: Tuna (fish-1), Salmon (fish-2), Trout (fish-3)
250+
fishOptions[0].selected = false; // deselect fish-1
251+
fishOptions[1].selected = true; // fish-2
252+
fishOptions[2].selected = true; // fish-3
253+
fireEvent.change(listbox);
254+
expect(onChange).toHaveBeenCalledWith(["fish-2", "fish-3"]);
255+
});
176256
it("should handle integer array items", () => {
177257
const schema = {
178258
type: "array" as const,

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

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,55 @@ describe("DynamicJsonForm String Fields", () => {
202202
fireEvent.change(select, { target: { value: "amber" } });
203203
expect(onChange).toHaveBeenCalledWith("amber");
204204
});
205+
206+
it("should render anyOf with const/title for labeled options and show description", () => {
207+
const onChange = jest.fn();
208+
const schema: JsonSchemaType = {
209+
type: "string",
210+
title: "Heroes",
211+
description: "Choose a hero",
212+
anyOf: [
213+
{ const: "hero-1", title: "Superman" },
214+
{ const: "hero-2", title: "Batman" },
215+
],
216+
};
217+
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
218+
219+
// Description should be visible above the select
220+
expect(screen.getByText("Choose a hero")).toBeInTheDocument();
221+
const select = screen.getByRole("combobox");
222+
const options = screen.getAllByRole("option");
223+
expect(options).toHaveLength(3);
224+
expect(options[1]).toHaveProperty("textContent", "Superman");
225+
expect(options[2]).toHaveProperty("textContent", "Batman");
226+
227+
fireEvent.change(select, { target: { value: "hero-2" } });
228+
expect(onChange).toHaveBeenCalledWith("hero-2");
229+
});
230+
231+
it("should render legacy enum with enumNames as labels", () => {
232+
const onChange = jest.fn();
233+
const schema: JsonSchemaType = {
234+
type: "string",
235+
title: "Pets",
236+
description: "Choose a pet",
237+
enum: ["pet-1", "pet-2", "pet-3"],
238+
enumNames: ["Cat", "Dog", "Bird"],
239+
} as unknown as JsonSchemaType; // enumNames is legacy extension
240+
241+
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
242+
243+
// Description should be visible above the select
244+
expect(screen.getByText("Choose a pet")).toBeInTheDocument();
245+
const options = screen.getAllByRole("option");
246+
expect(options[1]).toHaveProperty("textContent", "Cat");
247+
expect(options[2]).toHaveProperty("textContent", "Dog");
248+
expect(options[3]).toHaveProperty("textContent", "Bird");
249+
250+
const select = screen.getByRole("combobox");
251+
fireEvent.change(select, { target: { value: "pet-2" } });
252+
expect(onChange).toHaveBeenCalledWith("pet-2");
253+
});
205254
});
206255

207256
describe("Validation Attributes", () => {
@@ -535,8 +584,8 @@ describe("DynamicJsonForm Object Fields", () => {
535584
<DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,
536585
);
537586

538-
const nameLabel = screen.getByText("name");
539-
const optionalLabel = screen.getByText("optional");
587+
const nameLabel = screen.getByText("Name");
588+
const optionalLabel = screen.getByText("Optional");
540589

541590
const nameInput = nameLabel.closest("div")?.querySelector("input");
542591
const optionalInput = optionalLabel
@@ -567,30 +616,46 @@ describe("DynamicJsonForm Complex Fields", () => {
567616
};
568617

569618
describe("Basic Operations", () => {
570-
it("should render textbox and autoformat button, but no switch-to-form button", () => {
619+
it("should allow switching to JSON mode and show copy/format buttons", () => {
571620
renderForm();
621+
622+
// Initially renders as a form with a Switch to JSON button
623+
const switchToJson = screen.getByRole("button", {
624+
name: /switch to json/i,
625+
});
626+
expect(switchToJson).toBeInTheDocument();
627+
628+
// Switch to JSON mode
629+
fireEvent.click(switchToJson);
630+
631+
// Now a textarea and JSON helpers should be visible
572632
const input = screen.getByRole("textbox");
573633
expect(input).toHaveProperty("type", "textarea");
574-
const buttons = screen.getAllByRole("button");
575-
expect(buttons).toHaveLength(2); // Copy JSON + Format JSON
576634
const copyButton = screen.getByRole("button", { name: /copy json/i });
577635
const formatButton = screen.getByRole("button", { name: /format json/i });
578636
expect(copyButton).toBeTruthy();
579637
expect(formatButton).toBeTruthy();
638+
// And a Switch to Form button should appear
639+
expect(
640+
screen.getByRole("button", { name: /switch to form/i }),
641+
).toBeInTheDocument();
580642
});
581643

582-
it("should pass changed values to onChange", () => {
644+
it("should pass changed values to onChange in JSON mode", () => {
583645
const onChange = jest.fn();
584646
renderForm({ onChange });
585647

648+
// Switch to JSON mode first
649+
fireEvent.click(screen.getByRole("button", { name: /switch to json/i }));
650+
586651
const input = screen.getByRole("textbox");
587652
fireEvent.change(input, {
588653
target: { value: `{ "nested": "i am string" }` },
589654
});
590655

591656
// The onChange handler is debounced when using the JSON view, so we need to wait a little bit
592-
waitFor(() => {
593-
expect(onChange).toHaveBeenCalledWith(`{ "nested": "i am string" }`);
657+
return waitFor(() => {
658+
expect(onChange).toHaveBeenCalledWith({ nested: "i am string" });
594659
});
595660
});
596661
});
@@ -626,6 +691,10 @@ describe("DynamicJsonForm Copy JSON Functionality", () => {
626691
it("should render Copy JSON button when in JSON mode", () => {
627692
renderFormInJsonMode();
628693

694+
// Switch to JSON mode to reveal copy/format buttons
695+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
696+
fireEvent.click(switchBtn);
697+
629698
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
630699
expect(copyButton).toBeTruthy();
631700
});
@@ -653,6 +722,10 @@ describe("DynamicJsonForm Copy JSON Functionality", () => {
653722

654723
renderFormInJsonMode({ value: testValue });
655724

725+
// Switch to JSON mode first
726+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
727+
fireEvent.click(switchBtn);
728+
656729
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
657730
fireEvent.click(copyButton);
658731

@@ -768,6 +841,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
768841
it("should return valid for valid JSON in JSON mode", () => {
769842
renderFormWithRef();
770843

844+
// Switch to JSON mode to enable textarea editing/validation
845+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
846+
fireEvent.click(switchBtn);
847+
771848
const validateButton = screen.getByTestId("validate-button");
772849
fireEvent.click(validateButton);
773850

@@ -778,6 +855,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
778855
it("should return invalid for malformed JSON in JSON mode", async () => {
779856
renderFormWithRef();
780857

858+
// Switch to JSON mode first
859+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
860+
fireEvent.click(switchBtn);
861+
781862
// Enter invalid JSON
782863
const textarea = screen.getByRole("textbox");
783864
fireEvent.change(textarea, { target: { value: '{ "invalid": json }' } });
@@ -799,6 +880,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
799880
it("should return valid for empty JSON in JSON mode", () => {
800881
renderFormWithRef();
801882

883+
// Switch to JSON mode first
884+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
885+
fireEvent.click(switchBtn);
886+
802887
// Clear the textarea
803888
const textarea = screen.getByRole("textbox");
804889
fireEvent.change(textarea, { target: { value: "" } });
@@ -813,6 +898,10 @@ describe("DynamicJsonForm Validation Functionality", () => {
813898
it("should set error state when validation fails", async () => {
814899
renderFormWithRef();
815900

901+
// Switch to JSON mode first
902+
const switchBtn = screen.getByRole("button", { name: /switch to json/i });
903+
fireEvent.click(switchBtn);
904+
816905
// Enter invalid JSON
817906
const textarea = screen.getByRole("textbox");
818907
fireEvent.change(textarea, {

0 commit comments

Comments
 (0)