Skip to content

Commit

Permalink
Address section basic (#2133)
Browse files Browse the repository at this point in the history
* implementation complete, need to get default value to work

* fixes the names of BasicAddressFields to use the correct names that correspond to the form objects.

* clean up

* console log

* imports

---------

Co-authored-by: Adam Loup <[email protected]>
  • Loading branch information
alaapbharadwaj and adamloup-enquizit authored Dec 20, 2024
1 parent 1c5837d commit 32aa873
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Suggestions } from 'suggestion/Suggestions';
import { AddressSuggestion, useAddressAutocomplete } from './useAddressAutocomplete';
import { ReactElement, KeyboardEvent, ChangeEvent, useRef, useState, useEffect } from 'react';
import { LocationCodedValues } from 'location';
import { Orientation } from 'components/Entry';
import { Orientation, Sizing } from 'components/Entry';

const renderSuggestion = (suggestion: AddressSuggestion) => (
<>
Expand All @@ -29,6 +29,7 @@ type Props = {
defaultValue?: string;
flexBox?: boolean;
orientation?: Orientation;
sizing?: Sizing;
error?: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: ChangeEvent<HTMLInputElement>) => void;
Expand Down Expand Up @@ -99,6 +100,7 @@ const AddressSuggestionInput = (props: Props): ReactElement => {
placeholder={props.placeholder}
autoComplete="off"
orientation={orientation}
sizing={props.sizing}
error={props.error}
onChange={handleOnChange}
onBlur={props.onBlur}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AdministrativeEntryFields } from 'apps/patient/data/administrative/Admi
import { NameEntryFields } from './name/NameEntryFields';
import { BasicRaceEthnicityFields } from './raceEthnicity/BasicEthnicityRaceFields';
import { BasicPersonalDetailsFields } from './personalDetails/BasicPersonalDetailsFields';
import { BasicAddressFields } from './address/BasicAddressFields';

export const AddPatientBasicForm = () => {
const { control } = useFormContext<BasicNewPatientEntry>();
Expand All @@ -30,6 +31,9 @@ export const AddPatientBasicForm = () => {
<Card id="personalDetails" title="Personal details">
<BasicPersonalDetailsFields />
</Card>
<Card id="address" title="Address">
<BasicAddressFields />
</Card>
<Card id="phoneEmail" title="Phone & email">
<BasicPhoneEmailFields />
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { AddressEntry } from 'apps/patient/data';
import { FormProvider, useForm } from 'react-hook-form';
import { BasicAddressFields } from './BasicAddressFields';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const mockLocationCodedValues = {
states: {
all: [{ name: 'StateName', value: '1' }]
},
counties: {
byState: (state: string) => [{ name: 'CountyName', value: '2' }]
},
countries: [{ name: 'CountryName', value: '3' }]
};

jest.mock('location/useLocationCodedValues', () => ({
useLocationCodedValues: () => mockLocationCodedValues
}));

const Fixture = () => {
const form = useForm<AddressEntry>({
mode: 'onBlur',
defaultValues: {
address1: undefined,
address2: undefined,
city: undefined,
state: undefined,
zipcode: undefined,
county: undefined,
country: undefined,
censusTract: undefined
}
});
return (
<FormProvider {...form}>
<BasicAddressFields />
</FormProvider>
);
};

describe('when entering address section', () => {
it('should render with proper labels', () => {
const { getByLabelText } = render(<Fixture />);

expect(getByLabelText('Street address 1')).toBeInTheDocument();
expect(getByLabelText('Street address 2')).toBeInTheDocument();
expect(getByLabelText('City')).toBeInTheDocument();
expect(getByLabelText('State')).toBeInTheDocument();
expect(getByLabelText('Zip')).toBeInTheDocument();
expect(getByLabelText('County')).toBeInTheDocument();
expect(getByLabelText('Census tract')).toBeInTheDocument();
expect(getByLabelText('Country')).toBeInTheDocument();
});

test.each([
{ value: '0000.00', valid: false },
{ value: '0001.00', valid: false },
{ value: '0001.01', valid: true },
{ value: '1000.00', valid: false },
{ value: '9999.99', valid: false },
{ value: '9999.98', valid: true },
{ value: '0001', valid: true },
{ value: '9999', valid: true },
{ value: '0000', valid: false },
{ value: '9999.00', valid: false },
{ value: '0001.99', valid: false },
{ value: '1234.56', valid: true }
])('should validate Census Tract format for value: $value', async ({ value, valid }) => {
const { getByLabelText, queryByText } = render(<Fixture />);
const censusTractInput = getByLabelText('Census tract');

userEvent.clear(censusTractInput);
userEvent.paste(censusTractInput, value);
userEvent.tab();

const validationMessage =
'The Census tract should be in numeric XXXX or XXXX.xx format where XXXX is the basic tract and xx is the suffix. XXXX ranges from 0001 to 9999. The suffix is limited to a range between .01 and .98.';

await waitFor(() => {
const validationError = queryByText(validationMessage);
if (valid) {
expect(validationError).not.toBeInTheDocument();
} else {
expect(validationError).toBeInTheDocument();
}
});
});
it('should validate address 1', async () => {
const { getByLabelText, queryByText } = render(<Fixture />);

const address = getByLabelText('Street address 1');
userEvent.clear(address);
userEvent.paste(
address,
'hsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhjhsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhj dkacindijsnjpasdfilksbdvsdovbadkhv zcasjkfasnj hasb fkasj asjks s jdasjaksdb fbashf asfasfaskbf as faskj bfkdsbfkasb f'
);
userEvent.tab();

const validationMessage = 'The Street address 1 only allows 100 characters';

await waitFor(() => {
const validationError = queryByText(validationMessage);
expect(validationError).toBeInTheDocument();
});
});
it('should validate address 2', async () => {
const { getByLabelText, queryByText } = render(<Fixture />);

const address = getByLabelText('Street address 2');
userEvent.clear(address);
userEvent.paste(
address,
'hsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhjhsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhj dkacindijsnjpasdfilksbdvsdovbadkhv zcasjkfasnj hasb fkasj asjks s jdasjaksdb fbashf asfasfaskbf as faskj bfkdsbfkasb f'
);
userEvent.tab();

const validationMessage = 'The Street address 2 only allows 100 characters';

await waitFor(() => {
const validationError = queryByText(validationMessage);
expect(validationError).toBeInTheDocument();
});
});
it('should validate city', async () => {
const { getByLabelText, queryByText } = render(<Fixture />);

const city = getByLabelText('City');
userEvent.clear(city);
userEvent.paste(
city,
'hsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhjhsdfjhsjfjshkfhskhfkhskjfhkjshfhsdskjhfjsdhfjshjfhjsdhfsdhdfjsdhfhjshjfsdhfsdfhjsfjhsjdfsfasdjhvmbsauhcjdbkashjiodjbkdsnachudihbjsnjacibhj dkacindijsnjpasdfilksbdvsdovbadkhv zcasjkfasnj hasb fkasj asjks s jdasjaksdb fbashf asfasfaskbf as faskj bfkdsbfkasb f'
);
userEvent.tab();

const validationMessage = 'The City only allows 100 characters';

await waitFor(() => {
const validationError = queryByText(validationMessage);
expect(validationError).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { AddressSuggestion, AddressSuggestionInput } from 'address/suggestion';
import { validCensusTractRule, CensusTractInputField } from 'apps/patient/data/address';
import { Input } from 'components/FormInputs/Input';
import { EntryFieldsProps } from 'design-system/entry';
import { SingleSelect } from 'design-system/select';
import { validZipCodeRule, ZipCodeInputField } from 'libs/demographics/location';
import { useLocationCodedValues } from 'location';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { maxLengthRule } from 'validation/entry';
import { BasicNewPatientEntry } from '../entry';

const STREET_ADDRESS_LABEL = 'Street address 1';
const STREET_ADDRESS_2_LABEL = 'Street address 2';
const CITY_LABEL = 'City';
const ZIP_LABEL = 'Zip';
const CENSUS_TRACT_LABEL = 'Census tract';

type AddressEntryFieldsProps = EntryFieldsProps;

export const BasicAddressFields = ({ orientation = 'horizontal' }: AddressEntryFieldsProps) => {
const { control, reset } = useFormContext<BasicNewPatientEntry>();
const location = useLocationCodedValues();
const selectedState = useWatch({ control, name: 'address.state' });
const enteredCity = useWatch({ control, name: 'address.city' });
const enteredZip = useWatch({ control, name: 'address.zipcode' });
const counties = location.counties.byState(selectedState?.value ?? '');

const handleSuggestionSelection = (selected: AddressSuggestion) => {
reset(
{
address: {
address1: selected.address1,
city: selected.city,
state: selected.state ?? undefined,
zipcode: selected.zip
}
},
{ keepDefaultValues: true }
);
};

return (
<section>
<Controller
control={control}
name="address.address1"
rules={maxLengthRule(100, STREET_ADDRESS_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<AddressSuggestionInput
label={STREET_ADDRESS_LABEL}
orientation={orientation}
sizing="compact"
id={name}
locations={location}
criteria={{
city: enteredCity ?? undefined,
state: selectedState?.value ?? undefined,
zip: enteredZip ?? undefined
}}
defaultValue={value ?? ''}
onChange={onChange}
onBlur={onBlur}
onSelection={handleSuggestionSelection}
error={error?.message}
/>
)}
/>
<Controller
control={control}
name="address.address2"
rules={maxLengthRule(100, STREET_ADDRESS_2_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Input
label={STREET_ADDRESS_2_LABEL}
orientation={orientation}
sizing="compact"
onChange={onChange}
onBlur={onBlur}
defaultValue={value}
type="text"
name={name}
htmlFor={name}
id={name}
error={error?.message}
/>
)}
/>
<Controller
control={control}
name="address.city"
rules={maxLengthRule(100, CITY_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Input
label={CITY_LABEL}
orientation={orientation}
sizing="compact"
onChange={onChange}
onBlur={onBlur}
defaultValue={value}
type="text"
name={name}
htmlFor={name}
id={name}
error={error?.message}
/>
)}
/>
<Controller
control={control}
name="address.state"
render={({ field: { onChange, value, name } }) => (
<SingleSelect
label="State"
orientation={orientation}
sizing="compact"
value={value}
onChange={onChange}
id={name}
name={name}
options={location.states.all}
/>
)}
/>
<Controller
control={control}
name="address.zipcode"
rules={validZipCodeRule(ZIP_LABEL)}
render={({ field: { onChange, value, name, onBlur }, fieldState: { error } }) => (
<ZipCodeInputField
id={name}
label={ZIP_LABEL}
value={value}
onChange={onChange}
onBlur={onBlur}
orientation={orientation}
sizing="compact"
error={error?.message}
/>
)}
/>
<Controller
control={control}
name="address.county"
render={({ field: { onChange, value, name } }) => (
<SingleSelect
label="County"
orientation={orientation}
sizing="compact"
value={value}
onChange={onChange}
id={name}
name={name}
options={counties}
/>
)}
/>
<Controller
control={control}
name="address.censusTract"
rules={validCensusTractRule(CENSUS_TRACT_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<CensusTractInputField
id={name}
label={CENSUS_TRACT_LABEL}
value={value}
onChange={onChange}
onBlur={onBlur}
orientation={orientation}
sizing="compact"
error={error?.message}
/>
)}
/>
<Controller
control={control}
name="address.country"
render={({ field: { onChange, value, name } }) => (
<SingleSelect
label="Country"
orientation={orientation}
sizing="compact"
value={value}
onChange={onChange}
id={name}
name={name}
options={location.countries}
autoComplete="off"
/>
)}
/>
</section>
);
};
6 changes: 6 additions & 0 deletions apps/modernization-ui/src/apps/patient/add/basic/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ const initial = (asOf: string = today()): BasicNewPatientEntry => ({
administrative: {
asOf: asOf
},
address: {
country: {
value: '840',
name: 'United States'
}
},
identifications: []
});

Expand Down
Loading

0 comments on commit 32aa873

Please sign in to comment.