diff --git a/aselo-webchat-react-app/package-lock.json b/aselo-webchat-react-app/package-lock.json index 1a99023570..d933e00bbd 100644 --- a/aselo-webchat-react-app/package-lock.json +++ b/aselo-webchat-react-app/package-lock.json @@ -31416,23 +31416,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", diff --git a/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Checkbox.test.tsx b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Checkbox.test.tsx new file mode 100644 index 0000000000..61b763033d --- /dev/null +++ b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Checkbox.test.tsx @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2021-2026 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { render, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FormInputType } from 'hrm-form-definitions'; + +import Checkbox from '../Checkbox'; +import { PreEngagementDataItem } from '../../../../store/definitions'; + +jest.mock('../../../../localization/LocalizedTemplate', () => ({ + __esModule: true, + default: ({ code }: { code: string }) => <>{code}, +})); + +describe('Checkbox component', () => { + const definition = { + name: 'terms', + type: FormInputType.Checkbox as FormInputType.Checkbox, + label: 'Accept terms', + }; + + const noError: PreEngagementDataItem = { value: false, error: null, dirty: false }; + const withError: PreEngagementDataItem = { value: false, error: 'You must accept the terms', dirty: true }; + + const getItem = (item: PreEngagementDataItem) => (_name: string) => item; + const handleChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the correct label', () => { + const { getByText } = render( + , + ); + expect(getByText('Accept terms')).toBeInTheDocument(); + }); + + it('does not render an error message when error is null', () => { + const { queryByText } = render( + , + ); + expect(queryByText('You must accept the terms')).not.toBeInTheDocument(); + }); + + it('renders an error message when error is not null', () => { + const { getByText } = render( + , + ); + expect(getByText('You must accept the terms')).toBeInTheDocument(); + }); + + it('calls handleChange on blur with name and checked value', () => { + const { getByRole } = render( + , + ); + const checkbox = getByRole('checkbox'); + fireEvent.blur(checkbox, { target: { checked: true } }); + expect(handleChange).toHaveBeenCalledWith({ name: 'terms', value: true }); + }); +}); diff --git a/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/DependentSelect.test.tsx b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/DependentSelect.test.tsx new file mode 100644 index 0000000000..5649dedf37 --- /dev/null +++ b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/DependentSelect.test.tsx @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2021-2026 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { render, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FormInputType } from 'hrm-form-definitions'; + +import DependentSelect from '../DependentSelect'; +import { PreEngagementDataItem } from '../../../../store/definitions'; + +jest.mock('../../../../localization/LocalizedTemplate', () => ({ + __esModule: true, + default: ({ code }: { code: string }) => <>{code}, +})); + +describe('DependentSelect component', () => { + const definition = { + name: 'state', + type: FormInputType.DependentSelect as FormInputType.DependentSelect, + label: 'State', + dependsOn: 'country', + options: { + US: [ + { value: 'CA', label: 'California' }, + { value: 'NY', label: 'New York' }, + ], + UK: [{ value: 'ENG', label: 'England' }], + }, + }; + + const noError: PreEngagementDataItem = { value: 'CA', error: null, dirty: false }; + const withError: PreEngagementDataItem = { value: '', error: 'Please select a state', dirty: true }; + + const makeGetItem = + (stateItem: PreEngagementDataItem, countryValue: string) => + (name: string): PreEngagementDataItem => { + if (name === 'country') return { value: countryValue, error: null, dirty: false }; + return stateItem; + }; + + const handleChange = jest.fn(); + const setItemValue = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the correct label', () => { + const { getByText } = render( + , + ); + expect(getByText('State')).toBeInTheDocument(); + }); + + it('does not render an error message when error is null', () => { + const { queryByText } = render( + , + ); + expect(queryByText('Please select a state')).not.toBeInTheDocument(); + }); + + it('renders an error message when error is not null', () => { + const { getByText } = render( + , + ); + expect(getByText('Please select a state')).toBeInTheDocument(); + }); + + it('calls handleChange on blur with name and selected value', () => { + const { getByRole } = render( + , + ); + const select = getByRole('combobox'); + fireEvent.blur(select, { target: { value: 'NY' } }); + expect(handleChange).toHaveBeenCalledWith({ name: 'state', value: 'NY' }); + }); + + it('calls setItemValue to blank the dependent select when the dependee value changes', () => { + const getItemWithUS = makeGetItem({ value: 'CA', error: null, dirty: false }, 'US'); + const getItemWithUK = makeGetItem({ value: 'CA', error: null, dirty: false }, 'UK'); + + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(setItemValue).toHaveBeenCalledWith({ name: 'state', value: 'ENG' }); + }); +}); diff --git a/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Input.test.tsx b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Input.test.tsx new file mode 100644 index 0000000000..b53fb9bfbc --- /dev/null +++ b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Input.test.tsx @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2021-2026 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { render, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FormInputType } from 'hrm-form-definitions'; + +import InputText from '../Input'; +import { PreEngagementDataItem } from '../../../../store/definitions'; + +jest.mock('../../../../localization/LocalizedTemplate', () => ({ + __esModule: true, + default: ({ code }: { code: string }) => <>{code}, +})); + +describe('Input component', () => { + const definition = { + name: 'friendlyName', + type: FormInputType.Input as FormInputType.Input | FormInputType.Email, + label: 'Full Name', + placeholder: 'Enter your name', + }; + + const noError: PreEngagementDataItem = { value: '', error: null, dirty: false }; + const withError: PreEngagementDataItem = { value: '', error: 'This field is required', dirty: true }; + + const getItem = (item: PreEngagementDataItem) => (_name: string) => item; + const handleChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the correct label', () => { + const { getByText } = render( + , + ); + expect(getByText('Full Name')).toBeInTheDocument(); + }); + + it('does not render an error message when error is null', () => { + const { queryByText } = render( + , + ); + expect(queryByText('This field is required')).not.toBeInTheDocument(); + }); + + it('renders an error message when error is not null', () => { + const { getByText } = render( + , + ); + expect(getByText('This field is required')).toBeInTheDocument(); + }); + + it('calls handleChange on blur with name and value', () => { + const { getByPlaceholderText } = render( + , + ); + const input = getByPlaceholderText('Enter your name'); + fireEvent.blur(input, { target: { value: 'John' } }); + expect(handleChange).toHaveBeenCalledWith({ name: 'friendlyName', value: 'John' }); + }); +}); diff --git a/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Select.test.tsx b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Select.test.tsx new file mode 100644 index 0000000000..1350393c21 --- /dev/null +++ b/aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Select.test.tsx @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2021-2026 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { render, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FormInputType } from 'hrm-form-definitions'; + +import Select from '../Select'; +import { PreEngagementDataItem } from '../../../../store/definitions'; + +jest.mock('../../../../localization/LocalizedTemplate', () => ({ + __esModule: true, + default: ({ code }: { code: string }) => <>{code}, +})); + +describe('Select component', () => { + const definition = { + name: 'category', + type: FormInputType.Select as FormInputType.Select, + label: 'Category', + options: [ + { value: '', label: 'Select...' }, + { value: 'opt1', label: 'Option 1' }, + { value: 'opt2', label: 'Option 2' }, + ], + }; + + const noError: PreEngagementDataItem = { value: '', error: null, dirty: false }; + const withError: PreEngagementDataItem = { value: '', error: 'Please select a category', dirty: true }; + + const getItem = (item: PreEngagementDataItem) => (_name: string) => item; + const handleChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the correct label', () => { + const { getByText } = render( + , + ); + expect(queryByText('Please select a category')).not.toBeInTheDocument(); + }); + + it('renders an error message when error is not null', () => { + const { getByText } = render( + , + ); + const select = getByRole('combobox'); + fireEvent.blur(select, { target: { value: 'opt1' } }); + expect(handleChange).toHaveBeenCalledWith({ name: 'category', value: 'opt1' }); + }); +}); diff --git a/aselo-webchat-react-app/src/components/forms/formInputs/validation.ts b/aselo-webchat-react-app/src/components/forms/formInputs/validation.ts index 94b9f28747..1cbfa56ed3 100644 --- a/aselo-webchat-react-app/src/components/forms/formInputs/validation.ts +++ b/aselo-webchat-react-app/src/components/forms/formInputs/validation.ts @@ -50,6 +50,10 @@ const validateEmailPattern = ({ return null; } + if (!value) { + return null; + } + const matches = (value as string).match(EMAIL_PATTERN); if (Boolean(matches?.length)) {