From f63815d9acf79de7050d5f036c797ba0046b1d63 Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Thu, 18 Apr 2024 16:19:54 +0530 Subject: [PATCH] Validate the model form --- app/src/client/app/utils/formHelpers.ts | 19 ++++++ .../client/components/DynamicFormBuilder.tsx | 21 +++--- app/src/client/hooks/useForm.ts | 16 ++--- .../client/tests/DynamicFormBuilder.test.tsx | 66 +++++++++++++++++-- app/src/client/tests/formHelper.test.ts | 56 ++++++++++++++++ 5 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 app/src/client/app/utils/formHelpers.ts create mode 100644 app/src/client/tests/formHelper.test.ts diff --git a/app/src/client/app/utils/formHelpers.ts b/app/src/client/app/utils/formHelpers.ts new file mode 100644 index 0000000..65752a9 --- /dev/null +++ b/app/src/client/app/utils/formHelpers.ts @@ -0,0 +1,19 @@ +type ValidationError = { + type: string; + loc: string[]; + msg: string; + input: any; + url: string; + ctx?: any; +}; + +type ErrorOutput = { [key: string]: string }; + +export function parseValidationErrors(errors: ValidationError[]): ErrorOutput { + const result: ErrorOutput = {}; + errors.forEach((error) => { + const key = error.loc[error.loc.length - 1]; // Using the last item in 'loc' array as the key + result[key] = error.msg; + }); + return result; +} diff --git a/app/src/client/components/DynamicFormBuilder.tsx b/app/src/client/components/DynamicFormBuilder.tsx index 322ba41..0d0ea03 100644 --- a/app/src/client/components/DynamicFormBuilder.tsx +++ b/app/src/client/components/DynamicFormBuilder.tsx @@ -4,8 +4,8 @@ import { JsonSchema } from '../interfaces/models'; import { TextInput } from './form/TextInput'; import { SelectInput } from './form/SelectInput'; import { validateForm } from '../services/commonService'; -import NotificationBox from '../components/NotificationBox'; -import { object } from 'zod'; +import { parseValidationErrors } from '../app/utils/formHelpers'; +import Loader from '../admin/common/Loader'; interface DynamicFormBuilderProps { jsonSchema: JsonSchema; @@ -14,8 +14,7 @@ interface DynamicFormBuilderProps { } const DynamicFormBuilder: React.FC = ({ jsonSchema, validationURL, onSuccessCallback }) => { - const { formData, handleChange } = useForm(jsonSchema); - const [error, setError] = useState(null); + const { formData, handleChange, formErrors, setFormErrors } = useForm(jsonSchema); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (event: React.FormEvent) => { @@ -23,12 +22,11 @@ const DynamicFormBuilder: React.FC = ({ jsonSchema, val setIsLoading(true); try { const response = await validateForm(formData, validationURL); - setError(null); onSuccessCallback(response); } catch (error: any) { - setError(error.message); - console.log(JSON.parse(error.message)); - console.log(typeof JSON.parse(error.message)); + const errorMsgObj = JSON.parse(error.message); + const errors = parseValidationErrors(errorMsgObj); + setFormErrors(errors); } setIsLoading(false); }; @@ -55,6 +53,7 @@ const DynamicFormBuilder: React.FC = ({ jsonSchema, val onChange={(value) => handleChange(key, value)} /> )} + {formErrors[key] &&
{formErrors[key]}
} ) )} @@ -69,7 +68,11 @@ const DynamicFormBuilder: React.FC = ({ jsonSchema, val - {error && setError(null)} message={error} />} + {isLoading && ( +
+ +
+ )} ); }; diff --git a/app/src/client/hooks/useForm.ts b/app/src/client/hooks/useForm.ts index 1701ec6..9b04e87 100644 --- a/app/src/client/hooks/useForm.ts +++ b/app/src/client/hooks/useForm.ts @@ -1,33 +1,33 @@ -// hooks/useForm.ts import { useState, useEffect } from 'react'; import { JsonSchema } from '../interfaces/models'; export const useForm = (jsonSchema: JsonSchema) => { const [formData, setFormData] = useState<{ [key: string]: any }>({}); + const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); useEffect(() => { const initialValues: { [key: string]: any } = {}; Object.keys(jsonSchema.properties).forEach((key) => { const property = jsonSchema.properties[key]; - // Check for enum with exactly one value and set it, otherwise use default or fallback to an empty string if (property.enum && property.enum.length === 1) { - initialValues[key] = property.enum[0]; // Auto-set single enum value + initialValues[key] = property.enum[0]; } else { - initialValues[key] = property.default ?? ''; // Use default or empty string if no default + initialValues[key] = property.default ?? ''; } }); setFormData(initialValues); + setFormErrors({}); // Reset errors on schema change }, [jsonSchema]); const handleChange = (key: string, value: any) => { - setFormData((prev) => ({ - ...prev, - [key]: value, - })); + setFormData((prev) => ({ ...prev, [key]: value })); + setFormErrors((prev) => ({ ...prev, [key]: '' })); // Clear error on change }; return { formData, handleChange, + formErrors, + setFormErrors, // Expose this to allow setting errors from the component }; }; diff --git a/app/src/client/tests/DynamicFormBuilder.test.tsx b/app/src/client/tests/DynamicFormBuilder.test.tsx index 2f3f909..d370fd9 100644 --- a/app/src/client/tests/DynamicFormBuilder.test.tsx +++ b/app/src/client/tests/DynamicFormBuilder.test.tsx @@ -7,6 +7,8 @@ import DynamicFormBuilder from '../components/DynamicFormBuilder'; import { JsonSchema } from '../interfaces/models'; import { validateForm } from '../services/commonService'; +const setFormErrors = vi.fn(); +const handleChange = vi.fn(); vi.mock('../hooks/useForm', () => ({ useForm: () => ({ formData: { @@ -15,7 +17,9 @@ vi.mock('../hooks/useForm', () => ({ base_url: 'https://api.openai.com/v1', api_type: 'openai', }, - handleChange: vi.fn(), + handleChange, + formErrors: {}, + setFormErrors, }), })); @@ -92,9 +96,38 @@ describe('DynamicFormBuilder', () => { expect(onSuccessCallback).toHaveBeenCalled(); }); - test('shows an error message when submission fails', async () => { - // Mock the validateForm to simulate a failure - vi.mocked(validateForm).mockImplementationOnce(() => Promise.reject(new Error('Failed to validate'))); + // test('shows an error message when submission fails', async () => { + // // Mock the validateForm to simulate a failure + // vi.mocked(validateForm).mockImplementationOnce(() => Promise.reject(new Error('Failed to validate'))); + // const onSuccessCallback = vi.fn(); + // renderInContext( + // + // ); + // await fireEvent.submit(screen.getByRole('button', { name: /submit/i })); + // expect(onSuccessCallback).not.toHaveBeenCalled(); + // }); + + test('displays validation errors next to form fields', async () => { + const validationErrorMessage = 'This field is required'; + // Mock validateForm to simulate an API error response + vi.mocked(validateForm).mockRejectedValueOnce( + new Error( + JSON.stringify([ + { + type: 'string_type', + loc: ['body', 'api_key'], + msg: validationErrorMessage, + input: 1, + url: 'https://errors.pydantic.dev/2.7/v/string_type', + }, + ]) + ) + ); + const onSuccessCallback = vi.fn(); renderInContext( { onSuccessCallback={onSuccessCallback} /> ); - await fireEvent.submit(screen.getByRole('button', { name: /submit/i })); + + await act(async () => { + fireEvent.click(screen.getByTestId('form-submit-button')); + }); + expect(onSuccessCallback).not.toHaveBeenCalled(); + expect(setFormErrors).toHaveBeenCalledWith({ + api_key: 'This field is required', + }); + }); + test('handles field changes', async () => { + renderInContext( + + ); + + const input = screen.getByLabelText('API Key'); + await act(async () => { + fireEvent.change(input, { target: { value: 'new-key' } }); + }); + + expect(handleChange).toHaveBeenCalledWith('api_key', 'new-key'); }); }); diff --git a/app/src/client/tests/formHelper.test.ts b/app/src/client/tests/formHelper.test.ts new file mode 100644 index 0000000..69fb854 --- /dev/null +++ b/app/src/client/tests/formHelper.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { parseValidationErrors } from '../app/utils/formHelpers'; + +describe('parseValidationErrors', () => { + it('converts a list of Pydantic validation errors to a simplified error object', () => { + const input = [ + { + type: 'string_type', + loc: ['body', 'api_key'], + msg: 'Input should be a valid string', + input: 4, + url: 'https://errors.pydantic.dev/2.7/v/string_type', + }, + { + type: 'url_parsing', + loc: ['body', 'base_url'], + msg: 'Input should be a valid URL, relative URL without a base', + input: '1', + ctx: { + error: 'relative URL without a base', + }, + url: 'https://errors.pydantic.dev/2.7/v/url_parsing', + }, + ]; + + const expectedOutput = { + api_key: 'Input should be a valid string', + base_url: 'Input should be a valid URL, relative URL without a base', + }; + + const result = parseValidationErrors(input); + expect(result).toEqual(expectedOutput); + }); + + it('converts a list of Pydantic validation errors to a simplified error object', () => { + const input = [ + { + type: 'url_parsing', + loc: ['body', 'base_url'], + msg: 'Input should be a valid URL, relative URL without a base', + input: '1', + ctx: { + error: 'relative URL without a base', + }, + url: 'https://errors.pydantic.dev/2.7/v/url_parsing', + }, + ]; + + const expectedOutput = { + base_url: 'Input should be a valid URL, relative URL without a base', + }; + + const result = parseValidationErrors(input); + expect(result).toEqual(expectedOutput); + }); +});