diff --git a/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts b/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts index 08cd3ded5..15460661e 100644 --- a/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts +++ b/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts @@ -1,5 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../../fixtures/testFixture'; +import { DefaultInputCharLimit } from 'utils/constants'; +import { assertTextInput } from '../../../utils/inputValidators'; +import { controlUnicode } from '../../../utils/unicodeConstants'; test('Complete the User Profile: Checking for form errors based on user input', async ({ page, @@ -33,3 +36,40 @@ test('Complete the User Profile: Checking for form errors based on user input', ); }); }); + +test('Complete the User Profile: Checking for input length restriction', async ({ + page, +}) => { + test.slow(); + + await test.step('Complete the User Profile: Check that the error header render when no input is filled', async () => { + await page.getByLabel('Submit User Profile').click(); + await expect(page.locator('#step1FormErrorHeader div').first()).toBeVisible( + { + timeout: 30_000, + }, + ); + }); + + await test.step('Complete the User Profile: Check the first and last names for invalid input', async () => { + const expectedValues = { + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + }; + const unexpectedValues = { + firstField: controlUnicode, + lastField: controlUnicode, + }; + + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + }); +}); diff --git a/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts b/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts index 4bf896c32..564e921bc 100644 --- a/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts +++ b/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts @@ -1,5 +1,16 @@ import { expect } from '@playwright/test'; import { test } from '../../../fixtures/testFixture'; +import { controlUnicode } from '../../../utils/unicodeConstants'; +import { + assertTextInput, + assertSelectInput, +} from '../../../utils/inputValidators'; +import { + DefaultInputCharLimit, + PhoneInputCharLimit, + EmailInputCharLimit, + ZipInputCharLimit, +} from 'utils/constants'; test('Point of Contact: Checking for form errors based on user input', async ({ page, @@ -37,3 +48,133 @@ test('Point of Contact: Checking for form errors based on user input', async ({ ); }); }); + +test('Point of Contact: Checking for unicode tolerance based on user input', async ({ + page, + navigateToProvidePointOfContact, +}) => { + test.slow(); + + navigateToProvidePointOfContact; + + await test.step('Point of Contact: Check that the error header render when no input is filled', async () => { + await page.getByRole('button', { name: 'Continue to next step' }).click(); + await expect( + page.locator('#PointOfContactFormErrors div').first(), + ).toBeVisible(); + }); + + await test.step('Point of Contact: Check the first and last names for invalid input', async () => { + const expectedValues = { + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + phoneField: controlUnicode.slice(0, PhoneInputCharLimit), + extensionField: controlUnicode.slice(0, DefaultInputCharLimit), + emailField: controlUnicode.slice(0, EmailInputCharLimit), + addressField1: controlUnicode.slice(0, DefaultInputCharLimit), + addressField2: controlUnicode.slice(0, DefaultInputCharLimit), + addressField3: controlUnicode.slice(0, DefaultInputCharLimit), + addressField4: controlUnicode.slice(0, DefaultInputCharLimit), + cityField: controlUnicode.slice(0, DefaultInputCharLimit), + stateField: 'TX', + zipField: controlUnicode.slice(0, ZipInputCharLimit), + }; + const unexpectedValues = { + firstField: controlUnicode, + lastField: controlUnicode, + phoneField: controlUnicode, + extensionField: controlUnicode, + emailField: controlUnicode, + addressField1: controlUnicode, + addressField2: controlUnicode, + addressField3: controlUnicode, + addressField4: controlUnicode, + cityField: controlUnicode, + stateField: '', + zipField: controlUnicode, + }; + + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + await assertTextInput(page, 'Phone number', { + fill: controlUnicode, + expected: expectedValues.phoneField, + unexpected: unexpectedValues.phoneField, + }); + await assertTextInput(page, 'Extension', { + fill: controlUnicode, + expected: expectedValues.extensionField, + unexpected: unexpectedValues.extensionField, + }); + await assertTextInput(page, 'Email address', { + fill: controlUnicode, + expected: expectedValues.emailField, + unexpected: unexpectedValues.emailField, + }); + await assertTextInput(page, 'Street address line 1', { + fill: controlUnicode, + expected: expectedValues.addressField1, + unexpected: unexpectedValues.addressField1, + }); + await assertTextInput(page, 'Street address line 2', { + fill: controlUnicode, + expected: expectedValues.addressField2, + unexpected: unexpectedValues.addressField2, + }); + await assertTextInput(page, 'Street address line 3', { + fill: controlUnicode, + expected: expectedValues.addressField3, + unexpected: unexpectedValues.addressField3, + }); + await assertTextInput(page, 'Street address line 4', { + fill: controlUnicode, + expected: expectedValues.addressField4, + unexpected: unexpectedValues.addressField4, + }); + await assertTextInput(page, 'City', { + fill: controlUnicode, + expected: expectedValues.cityField, + unexpected: unexpectedValues.cityField, + }); + await assertSelectInput(page, 'State or territory', { + fill: { label: 'Texas (TX)' }, + expected: expectedValues.stateField, + unexpected: unexpectedValues.stateField, + }); + await assertTextInput(page, 'Zip code', { + fill: controlUnicode, + expected: expectedValues.zipField, + unexpected: unexpectedValues.zipField, + }); + + await page.getByRole('button', { name: 'Continue to next step' }).click(); + + await expect(page.locator('#PointOfContactFormErrors')).toContainText( + 'Enter a valid phone number', + ); + await expect(page.locator('#PointOfContactFormErrors')).toContainText( + 'Enter a valid email address', + ); + await expect(page.locator('#PointOfContactFormErrors')).toContainText( + 'Enter a valid ZIP code', + ); + + await expect(page.locator('form')).toContainText( + 'You must enter a valid phone number.', + ); + await expect(page.locator('form')).toContainText( + 'You must enter a valid email address.', + ); + await expect(page.locator('form')).toContainText( + 'You must enter a valid ZIP code.', + ); + }); +}); diff --git a/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts b/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts index e93fe637d..8f81f86e5 100644 --- a/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts +++ b/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../fixtures/testFixture'; import { controlUnicode } from '../../utils/unicodeConstants'; +import { assertTextInput } from '../../utils/inputValidators'; +import { DefaultInputCharLimit, LeiInputCharLimit } from 'utils/constants'; const expectedNoAssociationsSummaryUrl = /\/profile\/complete\/summary\/submitted$/; @@ -40,62 +42,37 @@ test('Complete User Profile with Bad Unicode -- No Associations -- process', asy await test.step('Fillout Complete User Profile (No Associations) with bad unicode and verify values', async () => { const expectedValues = { - firstField: controlUnicode.slice(0, 255), - lastField: controlUnicode.slice(0, 255), - // TODO: Update with correct value after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - finField: controlUnicode, - // TODO: Update with correct value after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - leiField: controlUnicode, + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + finField: controlUnicode.slice(0, DefaultInputCharLimit), + leiField: controlUnicode.slice(0, LeiInputCharLimit), }; const unexpectedValues = { firstField: controlUnicode, lastField: controlUnicode, - // TODO: Change to controlUnicode after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - finField: '', - // TODO: Change to controlUnicode after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - leiField: '', + finField: controlUnicode, + leiField: controlUnicode, }; - await page.getByLabel('First name').click(); - await page.getByLabel('First name').fill(controlUnicode); - - await page.getByLabel('Last name').click(); - await page.getByLabel('Last name').fill(controlUnicode); - - await page.getByLabel('Financial institution name').click(); - await page.getByLabel('Financial institution name').fill(controlUnicode); - - await page.getByLabel('Legal Entity Identifier (LEI)').click(); - await page.getByLabel('Legal Entity Identifier (LEI)').fill(controlUnicode); - - await expect(page.getByLabel('First name')).not.toHaveValue( - unexpectedValues.firstField, - ); - await expect(page.getByLabel('Last name')).not.toHaveValue( - unexpectedValues.lastField, - ); - await expect(page.getByLabel('Financial institution name')).not.toHaveValue( - unexpectedValues.finField, - ); - await expect( - page.getByLabel('Legal Entity Identifier (LEI)'), - ).not.toHaveValue(unexpectedValues.leiField); - - await expect(page.getByLabel('First name')).toHaveValue( - expectedValues.firstField, - ); - await expect(page.getByLabel('Last name')).toHaveValue( - expectedValues.lastField, - ); - await expect(page.getByLabel('Financial institution name')).toHaveValue( - expectedValues.finField, - ); - await expect(page.getByLabel('Legal Entity Identifier (LEI)')).toHaveValue( - expectedValues.leiField, - ); + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + await assertTextInput(page, 'Financial institution name', { + fill: controlUnicode, + expected: expectedValues.finField, + unexpected: unexpectedValues.finField, + }); + await assertTextInput(page, 'Legal Entity Identifier (LEI)', { + fill: controlUnicode, + expected: expectedValues.leiField, + unexpected: unexpectedValues.leiField, + }); }); }); diff --git a/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts b/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts index 240aa4b5e..6e5797225 100644 --- a/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts +++ b/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts @@ -1,5 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../fixtures/testFixture'; +import { DefaultInputCharLimit } from 'utils/constants'; +import { assertTextInput } from '../../utils/inputValidators'; +import { controlUnicode } from '../../utils/unicodeConstants'; test('Update Institution Profile Page', async ({ page, @@ -191,3 +194,110 @@ test('Update Institution Profile Page', async ({ }); }); }); + +test('Update Institution Profile Page: Check Character Limits', async ({ + page, + navigateToFilingHome, +}) => { + test.slow(); + + // Profile page + await test.step('User Profile Page', async () => { + navigateToFilingHome; + await page.goto('/profile/view'); + }); + + await test.step('Click: Institution link', async () => { + await page + .getByRole('link', { name: 'RegTech Regional Reserve - ' }) + .click(); + }); + + // Institution Profile page + await test.step('Institution Profile Page', async () => { + await page + .getByRole('link', { + name: 'Update your financial institution profile', + }) + .first() + .click(); + }); + + // Update Institution Profile page + await test.step('Update Institution Profile Page', async () => { + const expectedValues = { + otherField: controlUnicode.slice(0, DefaultInputCharLimit), + parentNameField: controlUnicode.slice(0, DefaultInputCharLimit), + parentLeiField: controlUnicode.slice(0, DefaultInputCharLimit), + parentRssdField: '', + topNameField: controlUnicode.slice(0, DefaultInputCharLimit), + topLeiField: controlUnicode.slice(0, DefaultInputCharLimit), + topRssdField: '', + }; + const unexpectedValues = { + otherField: controlUnicode, + parentNameField: controlUnicode, + parentLeiField: controlUnicode, + parentRssdField: controlUnicode, + topNameField: controlUnicode, + topLeiField: controlUnicode, + topRssdField: controlUnicode, + }; + + // Reset Form + await test.step('Click: Reset form', async () => { + await page.getByRole('button', { name: 'Reset form' }).click(); + await expect( + page.getByLabel('You must enter a type of'), + 'Other field reset', + ).not.toBeEnabled(); + }); + + await test.step('Click: Other checkbox', async () => { + await page.getByText('Other', { exact: true }).check(); + }); + + await assertTextInput(page, 'You must enter a type of', { + fill: controlUnicode, + expected: expectedValues.otherField, + unexpected: unexpectedValues.otherField, + }); + + await assertTextInput(page, '#parent_legal_name', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentNameField, + unexpected: unexpectedValues.parentNameField, + }); + await assertTextInput(page, '#parent_lei', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentLeiField, + unexpected: unexpectedValues.parentLeiField, + }); + await assertTextInput(page, '#parent_rssd_id', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentRssdField, + unexpected: unexpectedValues.parentRssdField, + }); + await assertTextInput(page, '#top_holder_legal_name', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topNameField, + unexpected: unexpectedValues.topNameField, + }); + await assertTextInput(page, '#top_holder_lei', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topLeiField, + unexpected: unexpectedValues.topLeiField, + }); + await assertTextInput(page, '#top_holder_rssd_id', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topRssdField, + unexpected: unexpectedValues.topRssdField, + }); + }); +}); diff --git a/e2e/utils/inputValidators.ts b/e2e/utils/inputValidators.ts new file mode 100644 index 000000000..2e5b2b0c4 --- /dev/null +++ b/e2e/utils/inputValidators.ts @@ -0,0 +1,73 @@ +import type { ElementHandle, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +export const assertTextInput = async ( + page: Page, + label: RegExp | string, + values: { + isLocator?: boolean; + fill: string; + expected: string; + unexpected: string; + }, +) => { + if (values?.isLocator) { + await page.locator(label as string).click(); + // Clear Input + await page.locator(label as string).fill(''); + await expect(page.locator(label as string)).toHaveValue(''); + // Fill Input + await page.locator(label as string).fill(values.fill); + await expect(page.locator(label as string)).not.toHaveValue( + values.unexpected, + ); + await expect(page.locator(label as string)).toHaveValue(values.expected); + } else { + await page.getByLabel(label).click(); + // Clear Input + await page.getByLabel(label).fill(''); + await expect(page.getByLabel(label)).toHaveValue(''); + // Fill Input + await page.getByLabel(label).fill(values.fill); + await expect(page.getByLabel(label)).not.toHaveValue(values.unexpected); + await expect(page.getByLabel(label)).toHaveValue(values.expected); + } +}; + +export const assertSelectInput = async ( + page: Page, + label: RegExp | string, + values: { + isLocator?: boolean; + fill: + | ElementHandle + | string + | readonly { + value?: string; + label?: string; + index?: number; + }[] + | readonly ElementHandle[] + | readonly string[] + | { + value?: string; + label?: string; + index?: number; + } + | null; + expected: string; + unexpected: string; + }, +) => { + if (values?.isLocator) { + await page.locator(label as string).selectOption(values.fill); + await expect(page.locator(label as string)).not.toHaveValue( + values.unexpected, + ); + await expect(page.locator(label as string)).toHaveValue(values.expected); + } else { + await page.getByLabel(label).selectOption(values.fill); + await expect(page.getByLabel(label)).not.toHaveValue(values.unexpected); + await expect(page.getByLabel(label)).toHaveValue(values.expected); + } +}; diff --git a/src/components/InputEntry.tsx b/src/components/InputEntry.tsx index 05d7c93d5..01946a385 100644 --- a/src/components/InputEntry.tsx +++ b/src/components/InputEntry.tsx @@ -1,17 +1,19 @@ /* eslint-disable react/require-default-props */ -import type { PropsWithoutRef, ReactNode } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { forwardRef } from 'react'; import { Element } from 'react-scroll'; import InputErrorMessage from 'components/InputErrorMessage'; import LabelOptional from 'components/LabelOptional'; -import { Heading, TextInput } from 'design-system-react'; +import { Heading } from 'design-system-react'; +import { TextInput } from 'components/TextInput'; import { DisplayField } from 'pages/Filing/ViewInstitutionProfile/DisplayField'; +import type { InputType } from 'design-system-react/dist/components/TextInput/TextInput'; -interface InputEntryProperties - extends PropsWithoutRef { +interface InputEntryProperties extends ComponentPropsWithoutRef<'input'> { id: string; label: JSX.Element | string; + type?: InputType; errorMessage?: string | undefined; isDisabled?: boolean; isLast?: boolean; @@ -27,6 +29,7 @@ const InputEntry = forwardRef( { className = '', id, + name = '', errorMessage, label, isDisabled = false, @@ -64,13 +67,10 @@ const InputEntry = forwardRef( {children} ( + ({ id, name, type = 'text', maxLength, ...properties }, reference) => { + const innerMaxLength = React.useMemo(() => { + if (maxLength && maxLength > 0) { + return maxLength; + } + switch (type) { + case 'url': { + return UrlInputCharLimit; + } + case 'tel': { + return PhoneInputCharLimit; + } + case 'email': { + return EmailInputCharLimit; + } + case 'number': + case 'password': + case 'search': + case 'text': { + return DefaultInputCharLimit; + } + default: { + return DefaultInputCharLimit; + } + } + }, [type, maxLength]); + + return ( + + ); + }, +); + +export default TextInput; diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 5a589701d..287e9b6d5 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -42,7 +42,7 @@ import type { PointOfContactSchema, } from 'types/formTypes'; import { ContactInfoMap, pointOfContactSchema } from 'types/formTypes'; -import { inputCharLimit } from 'utils/constants'; +import { PhoneInputCharLimit, ZipInputCharLimit } from 'utils/constants'; import useAddressStates from 'utils/useAddressStates'; import useFilingStatus from 'utils/useFilingStatus'; import useInstitutionDetails from 'utils/useInstitutionDetails'; @@ -272,7 +272,6 @@ function PointOfContact(): JSX.Element { label='First name' id='firstName' {...register('firstName')} - maxLength={inputCharLimit} errorMessage={formErrors.firstName?.message} showError /> @@ -280,7 +279,6 @@ function PointOfContact(): JSX.Element { label='Last name' id='lastName' {...register('lastName')} - maxLength={inputCharLimit} errorMessage={formErrors.lastName?.message} showError /> @@ -290,7 +288,9 @@ function PointOfContact(): JSX.Element { className='w-full bpMED:flex-[2]' label='Phone number' id='phone' + type='tel' {...register('phone')} + maxLength={PhoneInputCharLimit} helperText='Phone number must be in 555-555-5555 format.' errorMessage={formErrors.phone?.message} showError @@ -303,7 +303,6 @@ function PointOfContact(): JSX.Element { {...register('phoneExtension', { // onChange: handlePhoneExtensionInput, })} - maxLength={inputCharLimit} isOptional errorMessage={formErrors.phoneExtension?.message} showError @@ -313,6 +312,7 @@ function PointOfContact(): JSX.Element { diff --git a/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx b/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx index ce12aeaad..bfe68fa23 100644 --- a/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx +++ b/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx @@ -4,6 +4,7 @@ import { InstitutionHelperText } from 'pages/Filing/formHelpers'; import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import type { ValidationSchemaCPF } from 'types/formTypes'; import { formDelimiter } from 'utils/common'; +import { LeiInputCharLimit } from 'utils/constants'; interface AddFinancialInstitutionProperties { index: number; @@ -37,6 +38,7 @@ function AddFinancialInstitution({ // See `getAllProperties.tsx` for field naming convention id={`financialInstitutions${formDelimiter}${index}${formDelimiter}lei`} {...register(`financialInstitutions.${index}.lei` as const)} + maxLength={LeiInputCharLimit} errorMessage={ formErrors.financialInstitutions?.[`${index}`]?.lei?.message } diff --git a/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx b/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx index daa16ca85..c7f78054b 100644 --- a/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx +++ b/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx @@ -4,7 +4,6 @@ import InputEntry from 'components/InputEntry'; import { Link } from 'components/Link'; import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import type { BasicInfoSchema, ValidationSchema } from 'types/formTypes'; -import { inputCharLimit } from 'utils/constants'; // TODO: Refactor to take a generic and pass these TS schemas in type FormSchema = BasicInfoSchema | ValidationSchema; @@ -32,7 +31,6 @@ function Step1FormInfoFieldGroup({ label='First name' id='firstName' {...register('firstName')} - maxLength={inputCharLimit} errorMessage={formErrors.firstName?.message} showError /> @@ -40,7 +38,6 @@ function Step1FormInfoFieldGroup({ label='Last name' id='lastName' {...register('lastName')} - maxLength={inputCharLimit} errorMessage={formErrors.lastName?.message} showError /> @@ -48,6 +45,7 @@ function Step1FormInfoFieldGroup({