diff --git a/app/client/src/widgets/JSONFormWidget/component/Form.test.tsx b/app/client/src/widgets/JSONFormWidget/component/Form.test.tsx new file mode 100644 index 000000000000..40ece445aa06 --- /dev/null +++ b/app/client/src/widgets/JSONFormWidget/component/Form.test.tsx @@ -0,0 +1,198 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { klona } from "klona"; +import React from "react"; +import { useForm } from "react-hook-form"; +import { ROOT_SCHEMA_KEY, type SchemaItem } from "../constants"; +import Form from "./Form"; +import useFixedFooter from "./useFixedFooter"; + +jest.mock("react-hook-form", () => ({ + __esModule: true, + useForm: jest.fn(), + FormProvider: jest.fn(({ children }) =>
{children}
), +})); + +jest.mock("./useFixedFooter", () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock("widgets/ButtonWidget/component", () => ({ + __esModule: true, + BaseButton: jest.fn(({ children, ...props }) => ( + + )), +})); + +function mockUseForm(withErrors = false) { + (useForm as jest.Mock).mockReturnValue({ + formState: { + errors: withErrors + ? { + fieldName: { + type: "required", + message: "This field is required", + }, + } + : null, + }, + reset: jest.fn(), + trigger: jest.fn(), + watch: jest.fn(() => ({ + subscribe: jest.fn(), + unsubscribe: jest.fn(), + })), + }); +} + +const TEST_IDS = { + SUBMIT_BTN: "t--jsonform-submit-btn", + RESET_BTN: "t--jsonform-reset-btn", +}; + +const defaultProps = { + backgroundColor: "#fff", + disabledWhenInvalid: false, + fixedFooter: false, + getFormData: jest.fn(), + hideFooter: false, + isSubmitting: false, + isWidgetMounting: false, + onFormValidityUpdate: jest.fn(), + onSubmit: jest.fn(), + onreset: jest.fn(), + registerResetObserver: jest.fn(), + resetButtonLabel: "Reset", + resetButtonStyles: {}, + scrollContents: false, + showReset: true, + stretchBodyVertically: false, + submitButtonLabel: "Submit", + submitButtonStyles: {}, + title: "Test Form", + unregisterResetObserver: jest.fn(), + updateFormData: jest.fn(), + children: , +}; + +describe("Form Component", () => { + beforeEach(() => { + const mockBodyRef = React.createRef(); + const mockFooterRef = React.createRef(); + + (useFixedFooter as jest.Mock).mockReturnValue({ + bodyRef: mockBodyRef, + footerRef: mockFooterRef, + }); + }); + + describe("happy render path", () => { + beforeEach(() => mockUseForm()); + it("renders the form title", () => { + render(
Form Content
); + expect(screen.getByText("Test Form")).toBeInTheDocument(); + }); + + it("renders children inside the form body", () => { + render(
Form Content
); + expect(screen.getByText("Form Content")).toBeInTheDocument(); + }); + + it("renders the submit button with correct label", () => { + const { getByTestId } = render( +
Form Content
, + ); + const submitBtn = getByTestId(TEST_IDS.SUBMIT_BTN); + + expect(submitBtn).toBeInTheDocument(); + expect(submitBtn).toHaveAttribute("text", defaultProps.submitButtonLabel); + + fireEvent.click(submitBtn); + expect(defaultProps.onSubmit).toHaveBeenCalled(); + }); + + it("renders the reset button with correct label when showReset is true", () => { + const { getByTestId } = render( +
Form Content
, + ); + const resetBtn = getByTestId(TEST_IDS.RESET_BTN); + + expect(resetBtn).toBeInTheDocument(); + expect(resetBtn).toHaveAttribute("text", defaultProps.resetButtonLabel); + expect(defaultProps.registerResetObserver).toHaveBeenCalled(); + }); + + // Form updates data correctly when input values change + it("should update data when input values change", () => { + const mockUpdateFormData = jest.fn(); + const mockGetFormData = jest.fn().mockReturnValue({}); + const mockRegisterResetObserver = jest.fn(); + const mockUnregisterResetObserver = jest.fn(); + const mockOnSubmit = jest.fn(); + const mockOnFormValidityUpdate = jest.fn(); + const mockSchema = { [ROOT_SCHEMA_KEY]: {} as SchemaItem }; + const props = klona({ + ...defaultProps, + updateFormData: mockUpdateFormData, + getFormData: mockGetFormData, + registerResetObserver: mockRegisterResetObserver, + unregisterResetObserver: mockUnregisterResetObserver, + onSubmit: mockOnSubmit, + onFormValidityUpdate: mockOnFormValidityUpdate, + schema: mockSchema, + }); + const { getByTestId } = render(
); + + fireEvent.click(getByTestId(TEST_IDS.SUBMIT_BTN)); + expect(mockUpdateFormData).toHaveBeenCalled(); + }); + }); + + describe("unhappy render path", () => { + it("does not render the reset button when showReset is false", () => { + mockUseForm(); + const { queryByTestId } = render( + + Form Content +
, + ); + + expect(queryByTestId(TEST_IDS.RESET_BTN)).not.toBeInTheDocument(); + }); + + it("disables the submit button when disabledWhenInvalid is true and form is invalid", () => { + mockUseForm(true); + const { getByTestId } = render( +
+ Form Content +
, + ); + + expect(getByTestId(TEST_IDS.SUBMIT_BTN)).toBeDisabled(); + }); + + it("triggers a check, necessitating a re-render, when the children in form are updated", () => { + mockUseForm(true); + const propsToUpdate = klona({ + ...defaultProps, + disabledWhenInvalid: true, + }); + const { getByTestId } = render( +
Form Content
, + ); + + expect(getByTestId(TEST_IDS.SUBMIT_BTN)).toBeDisabled(); + }); + + it("should handle empty schema gracefully without errors", () => { + mockUseForm(); + const mockUpdateFormData = jest.fn(); + const props = klona({ ...defaultProps, schema: undefined }); + const { container } = render(
); + + expect(container).toBeInTheDocument(); + expect(mockUpdateFormData).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/client/src/widgets/JSONFormWidget/component/Form.tsx b/app/client/src/widgets/JSONFormWidget/component/Form.tsx index 5bd9b57cd022..353a0f528d77 100644 --- a/app/client/src/widgets/JSONFormWidget/component/Form.tsx +++ b/app/client/src/widgets/JSONFormWidget/component/Form.tsx @@ -299,6 +299,7 @@ function Form(