Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';

Check warning on line 1 in src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Lines of Code in a Single File

This module has 313 lines of code, improve code health by reducing it to 300 The number of Lines of Code in a single file. More Lines of Code lowers the code health.
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
Expand All @@ -23,12 +23,14 @@
single?: boolean;
readOnly?: boolean;
benefitsPlan?: MpdGoalBenefitsConstantPlanEnum;
geographicLocation?: string | null;
}

const TestComponent: React.FC<TestComponentProps> = ({
single = false,
readOnly = false,
benefitsPlan = MpdGoalBenefitsConstantPlanEnum.Base,
geographicLocation = null,
}) => (
<GqlMockedProvider<{
GoalCalculation: GoalCalculationQuery;
Expand All @@ -44,6 +46,7 @@
? MpdGoalBenefitsConstantSizeEnum.Single
: MpdGoalBenefitsConstantSizeEnum.MarriedNoChildren,
benefitsPlan,
geographicLocation,
},
},
GoalCalculatorConstants: {
Expand Down Expand Up @@ -324,6 +327,66 @@
);
});

it('does not save while the Geographic Location is cleared by typing', async () => {
mutationSpy.mockClear();
const { getByRole } = render(
<TestComponent geographicLocation="Orlando, FL" />,
);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).toHaveValue('Orlando, FL'));

userEvent.clear(input);

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation');
// The field stays empty instead of resetting mid-edit
expect(input).toHaveValue('');
});

it('saves null when the cleared Geographic Location loses focus', async () => {
mutationSpy.mockClear();
const { getByRole } = render(
<TestComponent geographicLocation="Orlando, FL" />,
);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).toHaveValue('Orlando, FL'));

userEvent.clear(input);
userEvent.tab();

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateGoalCalculation', {
input: {
accountListId: 'account-list-1',
attributes: {
id: 'goal-calculation-1',
geographicLocation: null,
},
},
}),
);
});

it('defaults to None and does not save when cleared and blurred without a saved location', async () => {
mutationSpy.mockClear();
const { getByRole } = render(<TestComponent />);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).not.toBeDisabled());
// An unset location displays as None
await waitFor(() => expect(input).toHaveValue('None'));

userEvent.clear(input);
userEvent.tab();

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation');
});

it('shows errors and does not save when input is invalid', async () => {
const { getByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import React, { useEffect, useMemo } from 'react';
import InfoIcon from '@mui/icons-material/Info';
import {
Autocomplete,
Grid,
IconButton,
MenuItem,
TextField,
Typography,
} from '@mui/material';
import { Grid, IconButton, MenuItem, Typography } from '@mui/material';
import { range } from 'lodash';
import { useTranslation } from 'react-i18next';
import * as yup from 'yup';
import { useGoalCalculator } from 'src/components/HrTools/GoalCalculator/Shared/GoalCalculatorContext';
import { DeferredClearAutocomplete } from 'src/components/HrTools/Shared/DeferredClearAutocomplete';
import {
GoalCalculationAge,
GoalCalculationRole,
Expand Down Expand Up @@ -148,22 +142,19 @@ export const InformationCategoryPersonalForm: React.FC<

{!isSpouse && (
<Grid size={12}>
<Autocomplete
<DeferredClearAutocomplete
options={locations}
value={geographicLocation ?? null}
onChange={(_, newValue) =>
saveField({ geographicLocation: newValue })
// While the constants load the options are empty, so any
// non-null value (including the 'None' fallback) would trigger
// MUI's "value not in options" dev warning
value={constantsLoading ? null : geographicLocation ?? 'None'}
onSave={(geographicLocation) =>
saveField({ geographicLocation })
}
disabled={!data || isReadOnly}
size="small"
renderInput={(params) => (
<TextField
{...params}
label={t('Geographic Location')}
helperText={t(
'Do you live within 50 miles of one of these major cities?',
)}
/>
disabled={!data || constantsLoading || isReadOnly}
label={t('Geographic Location')}
helperText={t(
'Do you live within 50 miles of one of these major cities?',
)}
/>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,23 @@ describe('calculationToFormValues', () => {
...baseCalculation,
age: null,
annualRequestedSalary: null,
geographicLocation: null,
calculationsYear: null,
});

expect(values.age).toBe('');
expect(values.annualRequestedSalary).toBe('');
expect(values.geographicLocation).toBe('');
expect(values.calculationsYear).toBe('');
});

it('defaults geographic location to None when unset', () => {
const values = calculationToFormValues({
...baseCalculation,
geographicLocation: null,
});

expect(values.geographicLocation).toBe('None');
});

describe('403(b) contribution default', () => {
const noContribution = {
...baseCalculation,
Expand Down Expand Up @@ -268,6 +275,17 @@ describe('formValuesToAttributes', () => {
expect(attributes.maritalStatus).toBeNull();
});

it('normalizes a geographic location of None to null on submit', () => {
// 'None' and null both mean no multiplier; null is the canonical stored
// form, so the display default of 'None' must not be persisted as-is.
const attributes = formValuesToAttributes({
...coupleValues,
geographicLocation: 'None',
});

expect(attributes.geographicLocation).toBeNull();
});

it('preserves a numeric 0 instead of dropping it to null', () => {
const attributes = formValuesToAttributes({
...coupleValues,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export const calculationToFormValues = (
staffConferenceTransfer: toNumberInput(calc.staffConferenceTransfer),
accountTransfers: toNumberInput(calc.accountTransfers),
advocacyTransfers: toNumberInput(calc.advocacyTransfers),
geographicLocation: calc.geographicLocation ?? '',
// An unset location defaults to the 'None' (0 multiplier) constant option
geographicLocation: calc.geographicLocation ?? 'None',
studentLoanMonthlyPayment: toNumberInput(calc.studentLoanMonthlyPayment),
carLoanMonthlyPayment: toNumberInput(calc.carLoanMonthlyPayment),
creditCardDebtMonthlyPayment: toNumberInput(
Expand Down Expand Up @@ -162,7 +163,12 @@ export const formValuesToAttributes = (
? toNumberOrNull(values.advocacyTransfers)
: null,

geographicLocation: values.geographicLocation || null,
// 'None' and null are equivalent (both mean no multiplier); null is the
// canonical stored form for NS, so normalize the display default back.
geographicLocation:
values.geographicLocation === 'None'
? null
: values.geographicLocation || null,

studentLoanMonthlyPayment: toNumberOrNull(values.studentLoanMonthlyPayment),
carLoanMonthlyPayment: toNumberOrNull(values.carLoanMonthlyPayment),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { MinistryDetails } from './MinistryDetails';

const TestComponent: React.FC<{ onCall?: MockLinkCallHandler }> = ({
onCall,
}) => (
<NsoMpdQuestionnaireTestWrapper onCall={onCall}>
const TestComponent: React.FC<{
onCall?: MockLinkCallHandler;
questionnaire?: React.ComponentProps<
typeof NsoMpdQuestionnaireTestWrapper
>['newStaffQuestionnaire'];
}> = ({ onCall, questionnaire }) => (
<NsoMpdQuestionnaireTestWrapper
onCall={onCall}
newStaffQuestionnaire={questionnaire}
>
<MinistryDetails />
</NsoMpdQuestionnaireTestWrapper>
);
Expand Down Expand Up @@ -101,6 +107,75 @@ describe('MinistryDetails', () => {
expect(getByRole('option', { name: 'Miami, FL' })).toBeInTheDocument();
});

it('defaults the city question to None and does not require an answer', async () => {
const { findByRole } = render(
<TestComponent questionnaire={{ geographicLocation: null }} />,
);

const cityCombobox = await findByRole('combobox', {
name: 'Is your ministry assignment location within 50 miles of one of these cities?',
});
await waitFor(() =>
expect(cityCombobox).not.toHaveAttribute('aria-disabled', 'true'),
);

expect(cityCombobox).toHaveTextContent('None');
expect(cityCombobox).not.toBeRequired();
});

it('shows no validation error when the city question is blurred without an answer', async () => {
const { findByRole, getByText, queryByText } = render(
<TestComponent questionnaire={{ geographicLocation: null }} />,
);

const cityCombobox = await findByRole('combobox', {
name: 'Is your ministry assignment location within 50 miles of one of these cities?',
});
await waitFor(() =>
expect(cityCombobox).not.toHaveAttribute('aria-disabled', 'true'),
);

cityCombobox.focus();
userEvent.tab();

expect(queryByText(/select an answer/i)).not.toBeInTheDocument();
// The guidance helper text remains in place of a validation error
expect(
getByText('If none of the locations apply, leave it as "None".'),
).toBeInTheDocument();
});

it('saves None when it is explicitly selected', async () => {
const mutationSpy = jest.fn();
const { findByRole } = render(
<TestComponent
onCall={mutationSpy}
questionnaire={{ geographicLocation: 'Miami, FL' }}
/>,
);

const cityCombobox = await findByRole('combobox', {
name: 'Is your ministry assignment location within 50 miles of one of these cities?',
});
await waitFor(() =>
expect(cityCombobox).not.toHaveAttribute('aria-disabled', 'true'),
);
userEvent.click(cityCombobox);
userEvent.click(await findByRole('option', { name: 'None' }));

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation(
'UpdateNewStaffQuestionnaire',
{
input: {
accountListId: 'account-list-1',
attributes: { geographicLocation: 'None' },
},
},
),
);
});

it('offers Field and Office assignment types', () => {
const { getByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,8 @@ export const MinistryDetails: React.FC = () => {
ministryLocation: yup
.string()
.required(t('Please enter an assignment location')),
geographicLocation: yup
.string()
.required(
t(
'Please select an answer. If none of the cities apply, select "None."',
),
),
// Optional: an unanswered question displays and calculates as "None"
geographicLocation: yup.string().nullable(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we want the choice to default to None? I thought I remembered @zweatshirt advocating for forcing the user to make an explicit choice. I'm not opposed to this change, I just want to make sure we have consensus.

assignmentType: yup
.string()
.required(t('Please select an assignment type')),
Expand Down Expand Up @@ -100,8 +95,10 @@ export const MinistryDetails: React.FC = () => {
)}
placeholder={t('Select a city')}
startAdornment={<LocationOn />}
helperText={t('If none of the locations apply, select "None."')}
helperText={t('If none of the locations apply, leave it as "None".')}
options={cities.map((city) => ({ value: city, label: city }))}
required={false}
emptyValue="None"
/>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const TestComponent: React.FC<{
helperText?: string;
errorText?: string;
disabled?: boolean;
}> = ({ helperText, errorText, disabled }) => (
emptyValue?: string;
}> = ({ helperText, errorText, disabled, emptyValue }) => (
<NsoMpdQuestionnaireTestWrapper>
<SelectQuestion
fieldName="geographicLocation"
Expand All @@ -29,6 +30,7 @@ const TestComponent: React.FC<{
helperText={helperText}
errorText={errorText}
disabled={disabled}
emptyValue={emptyValue}
/>
</NsoMpdQuestionnaireTestWrapper>
);
Expand All @@ -54,6 +56,21 @@ describe('SelectQuestion', () => {
expect(getByRole('option', { name: 'Select a city' })).toBeInTheDocument();
});

it('hides the placeholder when emptyValue is provided', async () => {
const { getByRole, findByRole, queryByRole } = render(
<TestComponent emptyValue="Atlanta, GA" />,
);

userEvent.click(getByRole('combobox', { name: 'Nearest city' }));

expect(
await findByRole('option', { name: 'Atlanta, GA' }),
).toBeInTheDocument();
expect(
queryByRole('option', { name: 'Select a city' }),
).not.toBeInTheDocument();
});

it('shows the guidance helper text', () => {
const { getByText } = render(
<TestComponent helperText="Choose the closest one." />,
Expand Down
Loading
Loading