From 911aab5f328c15685ac5d125acb883dbed56c023 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 24 Apr 2026 12:55:08 -0500 Subject: [PATCH 01/13] feat: add error message normalization utility --- .../components/ConfigurableRegistrationForm.jsx | 3 ++- src/register/data/utils.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx index 8c300b7ee7..65d9905b45 100644 --- a/src/register/components/ConfigurableRegistrationForm.jsx +++ b/src/register/components/ConfigurableRegistrationForm.jsx @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; import { FormFieldRenderer } from '../../field-renderer'; import { FIELDS } from '../data/constants'; import messages from '../messages'; +import { normalizeErrorMessage } from '../data/utils'; import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields'; /** @@ -97,7 +98,7 @@ const ConfigurableRegistrationForm = (props) => { const { name, value } = event.target; let error = ''; if ((!value || !value.trim()) && fieldDescriptions[name]?.error_message) { - error = fieldDescriptions[name].error_message; + error = normalizeErrorMessage(fieldDescriptions[name].error_message); } else if (name === 'confirm_email' && value !== email) { error = formatMessage(messages['email.do.not.match']); } diff --git a/src/register/data/utils.js b/src/register/data/utils.js index 25694527a6..fdbe37cf9b 100644 --- a/src/register/data/utils.js +++ b/src/register/data/utils.js @@ -6,6 +6,19 @@ import validateEmail from '../RegistrationFields/EmailField/validator'; import validateName from '../RegistrationFields/NameField/validator'; import validateUsername from '../RegistrationFields/UsernameField/validator'; +/** + * Normalizes a field error_message that may come from the backend as either + * a plain string or an object (e.g. { required: "Error text" }). + * @param {string|object} errorMessage + * @returns {string} + */ +export const normalizeErrorMessage = (errorMessage) => { + if (typeof errorMessage === 'object' && errorMessage !== null) { + return Object.values(errorMessage)[0] || ''; + } + return errorMessage || ''; +}; + /** * It validates the password field value * @param value @@ -96,7 +109,7 @@ export const isFormValid = ( if (key === 'country' && !configurableFormFields?.country?.displayValue) { fieldErrors[key] = formatMessage(messages['empty.country.field.error']); } else if (!configurableFormFields[key]) { - fieldErrors[key] = fieldDescriptions[key].error_message; + fieldErrors[key] = normalizeErrorMessage(fieldDescriptions[key].error_message); } if (fieldErrors[key]) { isValid = false; } }); From d2a5da9fc22d217a070a5f4cca11ab924b79021b Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 24 Apr 2026 13:00:21 -0500 Subject: [PATCH 02/13] feat: add placeholder support to FormFieldRenderer component --- src/field-renderer/FieldRenderer.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index c1158fa702..16bccd2284 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -59,6 +59,7 @@ const FormFieldRenderer = (props) => { as="textarea" name={fieldData.name} value={value} + placeholder={fieldData.placeholder} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} floatingLabel={fieldData.label} @@ -81,6 +82,7 @@ const FormFieldRenderer = (props) => { className={className} name={fieldData.name} value={value} + placeholder={fieldData.placeholder} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} floatingLabel={fieldData.label} @@ -142,6 +144,7 @@ FormFieldRenderer.propTypes = { type: PropTypes.string, label: PropTypes.string, name: PropTypes.string, + placeholder: PropTypes.string, options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), }).isRequired, onChangeHandler: PropTypes.func.isRequired, From ca57ca61251de878068cdb8164ccc4d86e80a200 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 24 Apr 2026 13:09:26 -0500 Subject: [PATCH 03/13] feat: enhance FormFieldRenderer with focus handling and help text feedback --- src/field-renderer/FieldRenderer.jsx | 50 ++++++++++++++++------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index 16bccd2284..e9442109e9 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -1,23 +1,42 @@ -import React from 'react'; +import React, { useState } from 'react'; -import { Form, Icon } from '@openedx/paragon'; +import { Form, Icon, TransitionReplace } from '@openedx/paragon'; import { ExpandMore } from '@openedx/paragon/icons'; import PropTypes from 'prop-types'; const FormFieldRenderer = (props) => { + const [hasFocus, setHasFocus] = useState(false); let formField = null; const { className, errorMessage, fieldData, onChangeHandler, isRequired, value, } = props; const handleFocus = (e) => { + setHasFocus(true); if (props.handleFocus) { props.handleFocus(e); } }; const handleOnBlur = (e) => { + setHasFocus(false); if (props.handleBlur) { props.handleBlur(e); } }; + const helpTextFeedback = ( + + {hasFocus && fieldData.instructions ? ( + + {fieldData.instructions} + + ) :
} + + ); + + const errorFeedback = isRequired && errorMessage ? ( + + {errorMessage} + + ) : null; + switch (fieldData.type) { case 'select': { if (!fieldData.options) { @@ -42,11 +61,7 @@ const FormFieldRenderer = (props) => { ))} - {isRequired && errorMessage && ( - - {errorMessage} - - )} + {errorFeedback} ); break; @@ -66,11 +81,8 @@ const FormFieldRenderer = (props) => { onBlur={handleOnBlur} onFocus={handleFocus} /> - {isRequired && errorMessage && ( - - {errorMessage} - - )} + {helpTextFeedback} + {errorFeedback} ); break; @@ -89,11 +101,8 @@ const FormFieldRenderer = (props) => { onBlur={handleOnBlur} onFocus={handleFocus} /> - {isRequired && errorMessage && ( - - {errorMessage} - - )} + {helpTextFeedback} + {errorFeedback} ); break; @@ -114,11 +123,7 @@ const FormFieldRenderer = (props) => { > {fieldData.label} - {isRequired && errorMessage && ( - - {errorMessage} - - )} + {errorFeedback} ); break; @@ -145,6 +150,7 @@ FormFieldRenderer.propTypes = { label: PropTypes.string, name: PropTypes.string, placeholder: PropTypes.string, + instructions: PropTypes.string, options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), }).isRequired, onChangeHandler: PropTypes.func.isRequired, From 8fc8e3251f812a97b287d203618ab71d700ab40e Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 24 Apr 2026 13:12:06 -0500 Subject: [PATCH 04/13] feat: add maxLength restriction to FormFieldRenderer for improved input validation --- src/field-renderer/FieldRenderer.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index e9442109e9..5c6ab921e2 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -75,6 +75,7 @@ const FormFieldRenderer = (props) => { name={fieldData.name} value={value} placeholder={fieldData.placeholder} + maxLength={fieldData.restrictions?.max_length} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} floatingLabel={fieldData.label} @@ -95,6 +96,7 @@ const FormFieldRenderer = (props) => { name={fieldData.name} value={value} placeholder={fieldData.placeholder} + maxLength={fieldData.restrictions?.max_length} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} floatingLabel={fieldData.label} @@ -151,6 +153,10 @@ FormFieldRenderer.propTypes = { name: PropTypes.string, placeholder: PropTypes.string, instructions: PropTypes.string, + restrictions: PropTypes.shape({ + max_length: PropTypes.number, + min_length: PropTypes.number, + }), options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), }).isRequired, onChangeHandler: PropTypes.func.isRequired, From 0469f1b7384b01ebaa2a64879d2f1ce7e4bebee1 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 10:30:49 -0500 Subject: [PATCH 05/13] fix: solve lint errors --- src/register/components/ConfigurableRegistrationForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx index 65d9905b45..35076dbcda 100644 --- a/src/register/components/ConfigurableRegistrationForm.jsx +++ b/src/register/components/ConfigurableRegistrationForm.jsx @@ -6,8 +6,8 @@ import PropTypes from 'prop-types'; import { FormFieldRenderer } from '../../field-renderer'; import { FIELDS } from '../data/constants'; -import messages from '../messages'; import { normalizeErrorMessage } from '../data/utils'; +import messages from '../messages'; import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields'; /** From 610a0a69d8bcbc00a5f737ebfbebea2303fcea48 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Apr 2026 16:22:32 -0500 Subject: [PATCH 06/13] test: add unit tests according changes --- .../tests/FieldRenderer.test.jsx | 148 ++++++++++++++++++ .../ConfigurableRegistrationForm.test.jsx | 31 ++++ src/register/data/tests/utils.test.js | 36 ++++- 3 files changed, 214 insertions(+), 1 deletion(-) diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx index 3d8797eec4..9e5c0b8aa0 100644 --- a/src/field-renderer/tests/FieldRenderer.test.jsx +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -200,4 +200,152 @@ describe('FieldRendererTests', () => { expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('You must agree to our Honor Code'); }); + + it('should render placeholder for text field', () => { + const fieldData = { + type: 'text', + label: 'Company', + name: 'company-field', + placeholder: 'Enter your company name', + }; + + const { container } = render(); + const input = container.querySelector('input#company-field'); + + expect(input.placeholder).toEqual('Enter your company name'); + }); + + it('should render placeholder for textarea field', () => { + const fieldData = { + type: 'textarea', + label: 'Goals', + name: 'goals-field', + placeholder: 'Share your learning goals', + }; + + const { container } = render(); + const input = container.querySelector('#goals-field'); + + expect(input.placeholder).toEqual('Share your learning goals'); + }); + + it('should apply maxLength restriction to text field', () => { + const fieldData = { + type: 'text', + label: 'Company', + name: 'company-field', + restrictions: { + max_length: 50, + }, + }; + + const { container } = render(); + const input = container.querySelector('input#company-field'); + + expect(input.maxLength).toEqual(50); + }); + + it('should apply maxLength restriction to textarea field', () => { + const fieldData = { + type: 'textarea', + label: 'Goals', + name: 'goals-field', + restrictions: { + max_length: 200, + }, + }; + + const { container } = render(); + const input = container.querySelector('#goals-field'); + + expect(input.maxLength).toEqual(200); + }); + + it('should show help text when field has focus and instructions are provided', () => { + const fieldData = { + type: 'text', + label: 'Username', + name: 'username-field', + instructions: 'Username must be between 2-30 characters', + }; + + const { container } = render(); + const input = container.querySelector('input#username-field'); + + // Help text should not be visible initially + expect(container.textContent).not.toContain('Username must be between 2-30 characters'); + + // Focus the field + fireEvent.focus(input); + + // Help text should now be visible + expect(container.textContent).toContain('Username must be between 2-30 characters'); + + // Blur the field + fireEvent.blur(input); + + // Help text should be hidden again + expect(container.textContent).not.toContain('Username must be between 2-30 characters'); + }); + + it('should show help text for textarea when focused', () => { + const fieldData = { + type: 'textarea', + label: 'Goals', + name: 'goals-field', + instructions: 'Please describe your learning goals in detail', + }; + + const { container } = render(); + const input = container.querySelector('#goals-field'); + + // Help text should not be visible initially + expect(container.textContent).not.toContain('Please describe your learning goals in detail'); + + // Focus the field + fireEvent.focus(input); + + // Help text should now be visible + expect(container.textContent).toContain('Please describe your learning goals in detail'); + }); + + it('should not show help text if instructions are not provided', () => { + const fieldData = { + type: 'text', + label: 'Company', + name: 'company-field', + }; + + const { container } = render(); + const input = container.querySelector('input#company-field'); + + // Focus the field + fireEvent.focus(input); + + // No help text should be rendered since instructions are not provided + const feedbackElement = container.querySelector('.form-control-feedback'); + expect(feedbackElement).toBeNull(); + }); + + it('should show help text for select field when focused', () => { + const fieldData = { + type: 'select', + label: 'Country', + name: 'country-field', + options: [['us', 'United States'], ['ca', 'Canada']], + instructions: 'Select your country of residence', + }; + + const { container } = render(); + const select = container.querySelector('select#country-field'); + + // Help text should not be visible initially + expect(container.textContent).not.toContain('Select your country of residence'); + + // Focus the field + fireEvent.focus(select); + + // Note: Select fields don't show help text in the current implementation + // This test documents the current behavior + }); }); diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx index 3d66426074..d6f8499386 100644 --- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx @@ -419,5 +419,36 @@ describe('ConfigurableRegistrationForm', () => { expect(professionErrorElement.textContent).toEqual(professionError); }); + + it('should normalize object-style error messages from backend', () => { + const professionErrorMessage = { required: 'Enter your profession' }; + + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + profession: { + name: 'profession', type: 'text', label: 'Profession', error_message: professionErrorMessage, + }, + }, + }, + }); + + const { getByLabelText, container } = render( + routerWrapper(reduxWrapper()), + ); + + const professionInput = getByLabelText('Profession'); + fireEvent.focus(professionInput); + fireEvent.blur(professionInput); + + const submitButton = container.querySelector('button.btn-brand'); + fireEvent.click(submitButton); + + const professionErrorElement = container.querySelector('#profession-error'); + + expect(professionErrorElement.textContent).toEqual('Enter your profession'); + }); }); }); diff --git a/src/register/data/tests/utils.test.js b/src/register/data/tests/utils.test.js index 56ef582ec8..70a6d7763a 100644 --- a/src/register/data/tests/utils.test.js +++ b/src/register/data/tests/utils.test.js @@ -1,4 +1,38 @@ -import { isFormValid } from '../utils'; +import { isFormValid, normalizeErrorMessage } from '../utils'; + +describe('normalizeErrorMessage', () => { + it('should return the error message if it is a string', () => { + const errorMessage = 'This field is required'; + expect(normalizeErrorMessage(errorMessage)).toBe('This field is required'); + }); + + it('should extract the first value from an object error message', () => { + const errorMessage = { required: 'This field is required' }; + expect(normalizeErrorMessage(errorMessage)).toBe('This field is required'); + }); + + it('should handle multiple keys in object and return first value', () => { + const errorMessage = { required: 'Required field', min_length: 'Too short' }; + const result = normalizeErrorMessage(errorMessage); + expect(['Required field', 'Too short']).toContain(result); + }); + + it('should return empty string if error message is null', () => { + expect(normalizeErrorMessage(null)).toBe(''); + }); + + it('should return empty string if error message is undefined', () => { + expect(normalizeErrorMessage(undefined)).toBe(''); + }); + + it('should return empty string if error message is an empty object', () => { + expect(normalizeErrorMessage({})).toBe(''); + }); + + it('should return empty string if error message is an empty string', () => { + expect(normalizeErrorMessage('')).toBe(''); + }); +}); describe('Payload validation', () => { let formatMessage; From b3f716506b11300c4a2d92b9b183427c802d114a Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Apr 2026 17:05:57 -0500 Subject: [PATCH 07/13] test: update FieldRenderer tests to handle asynchronous help text visibility --- src/field-renderer/tests/FieldRenderer.test.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx index 9e5c0b8aa0..4cdbf0da08 100644 --- a/src/field-renderer/tests/FieldRenderer.test.jsx +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { getConfig } from '@edx/frontend-platform'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import FieldRenderer from '../FieldRenderer'; @@ -261,7 +261,7 @@ describe('FieldRendererTests', () => { expect(input.maxLength).toEqual(200); }); - it('should show help text when field has focus and instructions are provided', () => { + it('should show help text when field has focus and instructions are provided', async () => { const fieldData = { type: 'text', label: 'Username', @@ -284,8 +284,10 @@ describe('FieldRendererTests', () => { // Blur the field fireEvent.blur(input); - // Help text should be hidden again - expect(container.textContent).not.toContain('Username must be between 2-30 characters'); + // Help text should be hidden again (wait for animation to complete) + await waitFor(() => { + expect(container.textContent).not.toContain('Username must be between 2-30 characters'); + }); }); it('should show help text for textarea when focused', () => { From 710e4de5ae9d693065da6a7c2591ad87c92614ed Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Apr 2026 18:46:12 -0500 Subject: [PATCH 08/13] refactor: update FieldRenderer to include help text feedback in form fields --- src/field-renderer/FieldRenderer.jsx | 46 +++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index 5c6ab921e2..b98bd01e75 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -56,13 +56,14 @@ const FormFieldRenderer = (props) => { onBlur={handleOnBlur} onFocus={handleFocus} > - - {fieldData.options.map(option => ( - - ))} - - {errorFeedback} - + + {fieldData.options.map(option => ( + + ))} + + {helpTextFeedback} + {errorFeedback} + ); break; } @@ -112,21 +113,22 @@ const FormFieldRenderer = (props) => { case 'checkbox': { formField = ( - onChangeHandler(e)} - onBlur={handleOnBlur} - onFocus={handleFocus} - > - {fieldData.label} - - {errorFeedback} - + onChangeHandler(e)} + onBlur={handleOnBlur} + onFocus={handleFocus} + > + {fieldData.label} + + {helpTextFeedback} + {errorFeedback} + ); break; } From 102798d1e7c2f344a1141cec436015498f2d36b6 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Apr 2026 10:41:28 -0500 Subject: [PATCH 09/13] refactor: enhance FieldRenderer to support options as both array and object formats --- src/field-renderer/FieldRenderer.jsx | 57 ++++++++++++++++------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index b98bd01e75..aa414742f5 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -42,6 +42,10 @@ const FormFieldRenderer = (props) => { if (!fieldData.options) { return null; } + const optionsArray = Array.isArray(fieldData.options) + ? fieldData.options + : Object.entries(fieldData.options); + formField = ( { onBlur={handleOnBlur} onFocus={handleFocus} > - - {fieldData.options.map(option => ( - - ))} - - {helpTextFeedback} - {errorFeedback} - + + {optionsArray.map(option => ( + + ))} + + {helpTextFeedback} + {errorFeedback} + ); break; } @@ -113,22 +117,22 @@ const FormFieldRenderer = (props) => { case 'checkbox': { formField = ( - onChangeHandler(e)} - onBlur={handleOnBlur} - onFocus={handleFocus} - > - {fieldData.label} - - {helpTextFeedback} - {errorFeedback} - + onChangeHandler(e)} + onBlur={handleOnBlur} + onFocus={handleFocus} + > + {fieldData.label} + + {helpTextFeedback} + {errorFeedback} + ); break; } @@ -159,7 +163,10 @@ FormFieldRenderer.propTypes = { max_length: PropTypes.number, min_length: PropTypes.number, }), - options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + options: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + PropTypes.objectOf(PropTypes.string), + ]), }).isRequired, onChangeHandler: PropTypes.func.isRequired, handleBlur: PropTypes.func, From cc0f791955edc9f3c5cc5943297dc9ab362f62c6 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Apr 2026 11:36:56 -0500 Subject: [PATCH 10/13] test: add tests for FieldRenderer to validate select field rendering and option handling --- .../tests/FieldRenderer.test.jsx | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx index 4cdbf0da08..4a9ffaf74a 100644 --- a/src/field-renderer/tests/FieldRenderer.test.jsx +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -350,4 +350,135 @@ describe('FieldRendererTests', () => { // Note: Select fields don't show help text in the current implementation // This test documents the current behavior }); + + it('should render select field with options as object format', () => { + const fieldData = { + type: 'select', + label: 'Favorite Language', + name: 'favorite-language-field', + options: { + '': '---------', + python: 'Python', + javascript: 'JavaScript', + java: 'Java', + go: 'Go', + }, + }; + + const { container } = render(); + const select = container.querySelector('select#favorite-language-field'); + const label = container.querySelector('label'); + + expect(select.type).toEqual('select-one'); + expect(label.textContent).toContain(fieldData.label); + + // Verify all options are rendered + const options = select.querySelectorAll('option'); + expect(options.length).toBeGreaterThan(0); + + // Check if specific options exist + const pythonOption = Array.from(options).find((opt) => opt.value === 'python'); + expect(pythonOption).toBeTruthy(); + expect(pythonOption.textContent).toEqual('Python'); + + const javascriptOption = Array.from(options).find((opt) => opt.value === 'javascript'); + expect(javascriptOption).toBeTruthy(); + expect(javascriptOption.textContent).toEqual('JavaScript'); + }); + + it('should handle selection change with object format options', () => { + const fieldData = { + type: 'select', + label: 'Favorite Language', + name: 'favorite-language-field', + options: { + '': '---------', + python: 'Python', + javascript: 'JavaScript', + java: 'Java', + go: 'Go', + }, + }; + + const { container } = render(); + const select = container.querySelector('select#favorite-language-field'); + + fireEvent.change(select, { target: { value: 'python' } }); + expect(value).toEqual('python'); + + fireEvent.change(select, { target: { value: 'javascript' } }); + expect(value).toEqual('javascript'); + }); + + it('should render all options correctly from object format', () => { + const fieldData = { + type: 'select', + label: 'Programming Language', + name: 'language-field', + options: { + '': '---------', + python: 'Python', + javascript: 'JavaScript', + java: 'Java', + go: 'Go', + }, + }; + + const { container } = render(); + const select = container.querySelector('select#language-field'); + const options = select.querySelectorAll('option'); + + // Should have default option + 5 from object (including empty string) + const optionValues = Array.from(options).map((opt) => opt.value); + const optionLabels = Array.from(options).map((opt) => opt.textContent); + + // Check that all expected values are present + expect(optionValues).toContain('python'); + expect(optionValues).toContain('javascript'); + expect(optionValues).toContain('java'); + expect(optionValues).toContain('go'); + + // Check that all expected labels are present + expect(optionLabels).toContain('Python'); + expect(optionLabels).toContain('JavaScript'); + expect(optionLabels).toContain('Java'); + expect(optionLabels).toContain('Go'); + }); + + it('should handle select field with array format options (backwards compatibility)', () => { + const fieldData = { + type: 'select', + label: 'Year', + name: 'year-field', + options: [ + ['2020', '2020'], + ['2021', '2021'], + ['2022', '2022'], + ], + }; + + const { container } = render(); + const select = container.querySelector('select#year-field'); + + fireEvent.change(select, { target: { value: '2021' } }); + expect(value).toEqual('2021'); + + // Verify options are rendered correctly + const options = select.querySelectorAll('option'); + const yearOption = Array.from(options).find((opt) => opt.value === '2021'); + expect(yearOption).toBeTruthy(); + expect(yearOption.textContent).toEqual('2021'); + }); + + it('should return null if options is not an array or object', () => { + const fieldData = { + type: 'select', + label: 'Invalid Options', + name: 'invalid-field', + options: null, + }; + + const { container } = render( {}} />); + expect(container.innerHTML).toEqual(''); + }); }); From ac753e18126318eb68c1907073af0567480ad9f8 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 11 May 2026 09:39:48 -0500 Subject: [PATCH 11/13] chore: address pr review --- src/field-renderer/FieldRenderer.jsx | 3 +- .../tests/FieldRenderer.test.jsx | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index aa414742f5..3f0c357b6a 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -27,7 +27,7 @@ const FormFieldRenderer = (props) => { {fieldData.instructions} - ) :
} + ) : } ); @@ -161,7 +161,6 @@ FormFieldRenderer.propTypes = { instructions: PropTypes.string, restrictions: PropTypes.shape({ max_length: PropTypes.number, - min_length: PropTypes.number, }), options: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx index 4a9ffaf74a..14877fe955 100644 --- a/src/field-renderer/tests/FieldRenderer.test.jsx +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -45,7 +45,7 @@ describe('FieldRendererTests', () => { name: 'yob-field', }; - const { container } = render( {}} />); + const { container } = render( { }} />); expect(container.innerHTML).toEqual(''); }); @@ -105,7 +105,7 @@ describe('FieldRendererTests', () => { type: 'unknown', }; - const { container } = render( {}} />); + const { container } = render( { }} />); expect(container.innerHTML).toContain(''); }); @@ -347,8 +347,8 @@ describe('FieldRendererTests', () => { // Focus the field fireEvent.focus(select); - // Note: Select fields don't show help text in the current implementation - // This test documents the current behavior + // Help text should now be visible + expect(container.textContent).toContain('Select your country of residence'); }); it('should render select field with options as object format', () => { @@ -400,14 +400,27 @@ describe('FieldRendererTests', () => { }, }; - const { container } = render(); + let capturedValue = ''; + + const mockChangeHandler = jest.fn((event) => { + capturedValue = event.target.value; + }); + + const { container } = render( + , + ); + const select = container.querySelector('select#favorite-language-field'); fireEvent.change(select, { target: { value: 'python' } }); - expect(value).toEqual('python'); + + expect(mockChangeHandler).toHaveBeenCalled(); + expect(capturedValue).toBe('python'); fireEvent.change(select, { target: { value: 'javascript' } }); - expect(value).toEqual('javascript'); + + expect(mockChangeHandler).toHaveBeenCalledTimes(2); + expect(capturedValue).toBe('javascript'); }); it('should render all options correctly from object format', () => { @@ -478,7 +491,7 @@ describe('FieldRendererTests', () => { options: null, }; - const { container } = render( {}} />); + const { container } = render( { }} />); expect(container.innerHTML).toEqual(''); }); }); From 3a726dff9911380cde217557e83fd1049da39040 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 13:52:49 -0500 Subject: [PATCH 12/13] feat: add minLength restriction to FormFieldRenderer for enhanced input validation --- src/field-renderer/FieldRenderer.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx index 3f0c357b6a..cc5cd3cc76 100644 --- a/src/field-renderer/FieldRenderer.jsx +++ b/src/field-renderer/FieldRenderer.jsx @@ -80,6 +80,7 @@ const FormFieldRenderer = (props) => { name={fieldData.name} value={value} placeholder={fieldData.placeholder} + minLength={fieldData.restrictions?.min_length} maxLength={fieldData.restrictions?.max_length} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} @@ -101,6 +102,7 @@ const FormFieldRenderer = (props) => { name={fieldData.name} value={value} placeholder={fieldData.placeholder} + minLength={fieldData.restrictions?.min_length} maxLength={fieldData.restrictions?.max_length} aria-invalid={isRequired && Boolean(errorMessage)} onChange={(e) => onChangeHandler(e)} @@ -160,6 +162,7 @@ FormFieldRenderer.propTypes = { placeholder: PropTypes.string, instructions: PropTypes.string, restrictions: PropTypes.shape({ + min_length: PropTypes.number, max_length: PropTypes.number, }), options: PropTypes.oneOfType([ From 2702cdea4e720fbcd5a39dbb2e7405950803e0e3 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 13:53:02 -0500 Subject: [PATCH 13/13] test: add tests for minLength restrictions in FieldRenderer --- .../tests/FieldRenderer.test.jsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx index 14877fe955..f3e828a258 100644 --- a/src/field-renderer/tests/FieldRenderer.test.jsx +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -261,6 +261,74 @@ describe('FieldRendererTests', () => { expect(input.maxLength).toEqual(200); }); + it('should apply minLength restriction to text field', () => { + const fieldData = { + type: 'text', + label: 'Username', + name: 'username-field', + restrictions: { + min_length: 3, + }, + }; + + const { container } = render(); + const input = container.querySelector('input#username-field'); + + expect(input.minLength).toEqual(3); + }); + + it('should apply minLength restriction to textarea field', () => { + const fieldData = { + type: 'textarea', + label: 'Goals', + name: 'goals-field', + restrictions: { + min_length: 10, + }, + }; + + const { container } = render(); + const input = container.querySelector('#goals-field'); + + expect(input.minLength).toEqual(10); + }); + + it('should apply both minLength and maxLength restrictions to text field', () => { + const fieldData = { + type: 'text', + label: 'Username', + name: 'username-field', + restrictions: { + min_length: 3, + max_length: 30, + }, + }; + + const { container } = render(); + const input = container.querySelector('input#username-field'); + + expect(input.minLength).toEqual(3); + expect(input.maxLength).toEqual(30); + }); + + it('should apply both minLength and maxLength restrictions to textarea field', () => { + const fieldData = { + type: 'textarea', + label: 'Goals', + name: 'goals-field', + restrictions: { + min_length: 10, + max_length: 200, + }, + }; + + const { container } = render(); + const input = container.querySelector('#goals-field'); + + expect(input.minLength).toEqual(10); + expect(input.maxLength).toEqual(200); + }); + it('should show help text when field has focus and instructions are provided', async () => { const fieldData = { type: 'text',