From f2fbc63951a48b77d3af1c30c5c004a373b8cca4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:19:35 +0000 Subject: [PATCH 1/3] Initial plan From 1d2ecf035daf6e750980d789d05a64f2071baa53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:33:29 +0000 Subject: [PATCH 2/3] test: add unit test coverage for pre-engagement form components [CHI-3696] Co-authored-by: gpaoloni <15805319+gpaoloni@users.noreply.github.com> --- aselo-webchat-react-app/package-lock.json | 17 --- .../src/components/PreEngagementFormPhase.tsx | 14 +- .../__tests__/PreEngagementFormPhase.test.tsx | 110 ++++++++++++++ .../formInputs/__tests__/Checkbox.test.tsx | 75 ++++++++++ .../__tests__/DependentSelect.test.tsx | 135 ++++++++++++++++++ .../forms/formInputs/__tests__/Input.test.tsx | 76 ++++++++++ .../formInputs/__tests__/Select.test.tsx | 80 +++++++++++ .../components/forms/formInputs/validation.ts | 4 + 8 files changed, 492 insertions(+), 19 deletions(-) create mode 100644 aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Checkbox.test.tsx create mode 100644 aselo-webchat-react-app/src/components/forms/formInputs/__tests__/DependentSelect.test.tsx create mode 100644 aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Input.test.tsx create mode 100644 aselo-webchat-react-app/src/components/forms/formInputs/__tests__/Select.test.tsx 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/PreEngagementFormPhase.tsx b/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx index 1082f7ca7d..355d7565c2 100644 --- a/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx +++ b/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx @@ -23,13 +23,14 @@ import { Text } from '@twilio-paste/core/text'; import { sessionDataHandler } from '../sessionDataHandler'; import { addNotification, changeEngagementPhase, updatePreEngagementDataField } from '../store/actions/genericActions'; import { initSession } from '../store/actions/initActions'; -import { AppState, EngagementPhase } from '../store/definitions'; +import { AppState, EngagementPhase, PreEngagementDataItem } from '../store/definitions'; import { Header } from './Header'; import { notifications } from '../notifications'; import { NotificationBar } from './NotificationBar'; import { fieldStyles, titleStyles, formStyles } from './styles/PreEngagementFormPhase.styles'; import LocalizedTemplate from '../localization/LocalizedTemplate'; -import { generateForm } from './forms/formInputs'; +import { generateForm, getDefaultValue } from './forms/formInputs'; +import { validateInput } from './forms/formInputs/validation'; export const PreEngagementFormPhase = () => { const { preEngagementData } = useSelector((state: AppState) => state.session ?? {}); @@ -46,6 +47,15 @@ export const PreEngagementFormPhase = () => { const handleSubmit = async (e: FormEvent) => { e.preventDefault(); + const fields = preEngagementFormDefinition.fields ?? []; + const hasErrors = fields.some(field => { + const item = getItem(field.name) as Partial; + const value = item.value ?? getDefaultValue(field); + return validateInput({ value, definition: field }) !== null; + }); + if (hasErrors) { + return; + } dispatch(changeEngagementPhase({ phase: EngagementPhase.Loading })); try { const data = await sessionDataHandler.fetchAndStoreNewSession({ diff --git a/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx b/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx index d8895432ad..75c7cc248e 100644 --- a/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx +++ b/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx @@ -190,4 +190,114 @@ describe('Pre Engagement Form Phase', () => { }, }); }); + + describe('form validation', () => { + const withCustomStore = (fields: AppState['config']['preEngagementFormDefinition']['fields']) => { + const customState: Partial = { + config: { + ...preloadedState.config, + preEngagementFormDefinition: { + description: 'Description', + submitLabel: 'Submit Label', + fields, + }, + }, + }; + const customStore = preloadStore(customState); + return (Component: React.ReactElement) => {Component}; + }; + + it('does not submit if a required Input field is empty', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([ + { name: 'friendlyName', type: FormInputType.Input, label: 'Name', required: true }, + ]); + const { container } = render(renderWithStore()); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + + it('does not submit if a required Email field is empty', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([ + { name: 'email', type: FormInputType.Email, label: 'Email', required: true }, + ]); + const { container } = render(renderWithStore()); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + + it('does not submit if an Email field value does not match the email pattern', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([{ name: 'email', type: FormInputType.Email, label: 'Email' }]); + const { container, getByLabelText } = render(renderWithStore()); + const emailInput = getByLabelText(/Email/); + fireEvent.blur(emailInput, { target: { value: 'not-an-email' } }); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + + it('does not submit if a required Select field is empty', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([ + { + name: 'category', + type: FormInputType.Select, + label: 'Category', + required: true, + options: [ + { value: '', label: 'Select...' }, + { value: 'opt1', label: 'Option 1' }, + ], + }, + ]); + const { container } = render(renderWithStore()); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + + it('does not submit if a required DependentSelect field is empty', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([ + { + name: 'country', + type: FormInputType.Select, + label: 'Country', + options: [{ value: 'US', label: 'United States' }], + }, + { + name: 'state', + type: FormInputType.DependentSelect, + label: 'State', + required: true, + dependsOn: 'country', + options: { US: [{ value: 'CA', label: 'California' }] }, + }, + ]); + const { container } = render(renderWithStore()); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + + it('does not submit if a required Checkbox field is unchecked', () => { + const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); + const renderWithStore = withCustomStore([ + { + name: 'terms', + type: FormInputType.Checkbox, + label: 'Accept terms', + required: { value: true, message: 'You must accept' }, + }, + ]); + const { container } = render(renderWithStore()); + const formBox = container.querySelector('form') as HTMLFormElement; + fireEvent.submit(formBox); + expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); + }); + }); }); 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)) { From dde68226015a4ac9494d64b32a1cf5d8f764871c Mon Sep 17 00:00:00 2001 From: Gianfranco Paoloni Date: Fri, 27 Feb 2026 17:51:05 -0300 Subject: [PATCH 3/3] revert: submit updates and tests --- .../src/components/PreEngagementFormPhase.tsx | 14 +-- .../__tests__/PreEngagementFormPhase.test.tsx | 110 ------------------ 2 files changed, 2 insertions(+), 122 deletions(-) diff --git a/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx b/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx index 355d7565c2..1082f7ca7d 100644 --- a/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx +++ b/aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx @@ -23,14 +23,13 @@ import { Text } from '@twilio-paste/core/text'; import { sessionDataHandler } from '../sessionDataHandler'; import { addNotification, changeEngagementPhase, updatePreEngagementDataField } from '../store/actions/genericActions'; import { initSession } from '../store/actions/initActions'; -import { AppState, EngagementPhase, PreEngagementDataItem } from '../store/definitions'; +import { AppState, EngagementPhase } from '../store/definitions'; import { Header } from './Header'; import { notifications } from '../notifications'; import { NotificationBar } from './NotificationBar'; import { fieldStyles, titleStyles, formStyles } from './styles/PreEngagementFormPhase.styles'; import LocalizedTemplate from '../localization/LocalizedTemplate'; -import { generateForm, getDefaultValue } from './forms/formInputs'; -import { validateInput } from './forms/formInputs/validation'; +import { generateForm } from './forms/formInputs'; export const PreEngagementFormPhase = () => { const { preEngagementData } = useSelector((state: AppState) => state.session ?? {}); @@ -47,15 +46,6 @@ export const PreEngagementFormPhase = () => { const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - const fields = preEngagementFormDefinition.fields ?? []; - const hasErrors = fields.some(field => { - const item = getItem(field.name) as Partial; - const value = item.value ?? getDefaultValue(field); - return validateInput({ value, definition: field }) !== null; - }); - if (hasErrors) { - return; - } dispatch(changeEngagementPhase({ phase: EngagementPhase.Loading })); try { const data = await sessionDataHandler.fetchAndStoreNewSession({ diff --git a/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx b/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx index 75c7cc248e..d8895432ad 100644 --- a/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx +++ b/aselo-webchat-react-app/src/components/__tests__/PreEngagementFormPhase.test.tsx @@ -190,114 +190,4 @@ describe('Pre Engagement Form Phase', () => { }, }); }); - - describe('form validation', () => { - const withCustomStore = (fields: AppState['config']['preEngagementFormDefinition']['fields']) => { - const customState: Partial = { - config: { - ...preloadedState.config, - preEngagementFormDefinition: { - description: 'Description', - submitLabel: 'Submit Label', - fields, - }, - }, - }; - const customStore = preloadStore(customState); - return (Component: React.ReactElement) => {Component}; - }; - - it('does not submit if a required Input field is empty', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([ - { name: 'friendlyName', type: FormInputType.Input, label: 'Name', required: true }, - ]); - const { container } = render(renderWithStore()); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - - it('does not submit if a required Email field is empty', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([ - { name: 'email', type: FormInputType.Email, label: 'Email', required: true }, - ]); - const { container } = render(renderWithStore()); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - - it('does not submit if an Email field value does not match the email pattern', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([{ name: 'email', type: FormInputType.Email, label: 'Email' }]); - const { container, getByLabelText } = render(renderWithStore()); - const emailInput = getByLabelText(/Email/); - fireEvent.blur(emailInput, { target: { value: 'not-an-email' } }); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - - it('does not submit if a required Select field is empty', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([ - { - name: 'category', - type: FormInputType.Select, - label: 'Category', - required: true, - options: [ - { value: '', label: 'Select...' }, - { value: 'opt1', label: 'Option 1' }, - ], - }, - ]); - const { container } = render(renderWithStore()); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - - it('does not submit if a required DependentSelect field is empty', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([ - { - name: 'country', - type: FormInputType.Select, - label: 'Country', - options: [{ value: 'US', label: 'United States' }], - }, - { - name: 'state', - type: FormInputType.DependentSelect, - label: 'State', - required: true, - dependsOn: 'country', - options: { US: [{ value: 'CA', label: 'California' }] }, - }, - ]); - const { container } = render(renderWithStore()); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - - it('does not submit if a required Checkbox field is unchecked', () => { - const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession'); - const renderWithStore = withCustomStore([ - { - name: 'terms', - type: FormInputType.Checkbox, - label: 'Accept terms', - required: { value: true, message: 'You must accept' }, - }, - ]); - const { container } = render(renderWithStore()); - const formBox = container.querySelector('form') as HTMLFormElement; - fireEvent.submit(formBox); - expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled(); - }); - }); });