From 67efa7d8fc345fc2f66b64e59e2bb9d3d3e67632 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 20 Aug 2026 10:58:38 -0400 Subject: [PATCH 1/6] Offer a guaranteed None geographic option instead of clearing Backspacing a geographic autocomplete to empty fired an extra mutation mid-typing because MUI clears the value the moment the input empties, resetting the field to None while the user was still typing. Seed a leading 'None' (0 multiplier) option in goalGeographicConstantMap and make the geographic autocompletes non-clearable, so selecting None is the way to unset and emptying the input just reverts on blur. Applied to the three surfaces with the bug: the PDS and MPD goal calculators and the Salary Calculator (via a new emptyValue prop on its generic AutosaveAutocomplete, since disableClearable needs an always-valid value). The NSO and NS selects never had the bug and are untouched; their dropdowns simply gain the None option from the seeded map. MPDX-9943 Co-Authored-By: Claude Fable 5 --- .../InformationCategory.test.tsx | 56 +++++++++++ .../InformationCategoryPersonalForm.tsx | 5 +- .../Setup/SetupStep.test.tsx | 97 +++++++++++++++++++ .../PdsGoalCalculator/Setup/SetupStep.tsx | 5 +- .../Autosave/AutosaveAutocomplete.tsx | 15 ++- .../PersonalInformationSection.test.tsx | 48 +++++++-- .../PersonalInformationSection.tsx | 1 + src/hooks/useGoalCalculatorConstants.test.tsx | 44 ++++++++- src/hooks/useGoalCalculatorConstants.ts | 2 + 9 files changed, 261 insertions(+), 12 deletions(-) diff --git a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx index 054f95aa7f..d544696aca 100644 --- a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx +++ b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx @@ -23,12 +23,14 @@ interface TestComponentProps { single?: boolean; readOnly?: boolean; benefitsPlan?: MpdGoalBenefitsConstantPlanEnum; + geographicLocation?: string | null; } const TestComponent: React.FC = ({ single = false, readOnly = false, benefitsPlan = MpdGoalBenefitsConstantPlanEnum.Base, + geographicLocation = null, }) => ( = ({ ? MpdGoalBenefitsConstantSizeEnum.Single : MpdGoalBenefitsConstantSizeEnum.MarriedNoChildren, benefitsPlan, + geographicLocation, }, }, GoalCalculatorConstants: { @@ -324,6 +327,59 @@ describe('InformationCategory', () => { ); }); + it('does not save while the Geographic Location is cleared by typing', async () => { + mutationSpy.mockClear(); + const { getByRole } = render( + , + ); + + 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('reverts to the saved Geographic Location when the cleared field loses focus', async () => { + mutationSpy.mockClear(); + const { getByRole } = render( + , + ); + + const input = getByRole('combobox', { name: 'Geographic Location' }); + await waitFor(() => expect(input).toHaveValue('Orlando, FL')); + + userEvent.clear(input); + userEvent.tab(); + + // The field is not clearable, so blurring restores the saved value + // without firing a mutation + await waitFor(() => expect(input).toHaveValue('Orlando, FL')); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation'); + }); + + it('defaults to None and does not save when cleared and blurred without a saved location', async () => { + mutationSpy.mockClear(); + const { getByRole } = render(); + + 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(); diff --git a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx index 0e117a1cb7..cde5e0b36f 100644 --- a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx +++ b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx @@ -150,7 +150,10 @@ export const InformationCategoryPersonalForm: React.FC< saveField({ geographicLocation: newValue }) } diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx index ee43cbd9fc..d0352c37f7 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx @@ -344,6 +344,103 @@ describe('SetupStep', () => { ); }); + it('does not save while the Geographic Multiplier is cleared by typing', async () => { + mutationSpy.mockClear(); + const { findByRole } = renderSetup({ + calculationMock: { + ...fullTimeSalariedMock, + geographicLocation: 'Orlando, FL', + }, + onCall: mutationSpy, + }); + + const input = await findByRole('combobox', { + name: 'Geographic Multiplier', + }); + await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)')); + + userEvent.clear(input); + + // Yield to the microtask queue so any pending mutation would have fired. + await new Promise((r) => setTimeout(r, 0)); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation'); + // The field stays empty instead of resetting to None mid-edit + expect(input).toHaveValue(''); + }); + + it('reverts to the saved Geographic Multiplier when the cleared field loses focus', async () => { + mutationSpy.mockClear(); + const { findByRole } = renderSetup({ + calculationMock: { + ...fullTimeSalariedMock, + geographicLocation: 'Orlando, FL', + }, + onCall: mutationSpy, + }); + + const input = await findByRole('combobox', { + name: 'Geographic Multiplier', + }); + await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)')); + + userEvent.clear(input); + userEvent.tab(); + + // The field is not clearable, so blurring restores the saved value + // without firing a mutation + await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)')); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation'); + }); + + it('saves None when it is explicitly selected', async () => { + mutationSpy.mockClear(); + const { findByRole } = renderSetup({ + calculationMock: { + ...fullTimeSalariedMock, + geographicLocation: 'Orlando, FL', + }, + onCall: mutationSpy, + }); + + const input = await findByRole('combobox', { + name: 'Geographic Multiplier', + }); + await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)')); + + userEvent.click(input); + userEvent.click(await findByRole('option', { name: 'None' })); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', { + attributes: { + id: 'goal-1', + geographicLocation: 'None', + }, + }), + ); + }); + + it('does not save when a Geographic Multiplier of None is cleared and loses focus', async () => { + mutationSpy.mockClear(); + const { findByRole } = renderSetup({ + calculationMock: fullTimeSalariedMock, + onCall: mutationSpy, + }); + + const input = await findByRole('combobox', { + name: 'Geographic Multiplier', + }); + await waitFor(() => expect(input).not.toBeDisabled()); + 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((r) => setTimeout(r, 0)); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation'); + }); + it('renders the Calculate my average hours button next to Hours Worked', async () => { const { findByRole } = renderSetup({ calculationMock: fullTimeHourlyMock, diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx index 0e690a2013..935a0eab31 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx @@ -281,9 +281,12 @@ export const SetupStep: React.FC = () => { + onChange={(_, newValue) => saveField({ geographicLocation: newValue }) } disabled={!calculation} diff --git a/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx b/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx index fa88ff40f7..69b0b15205 100644 --- a/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx +++ b/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx @@ -10,12 +10,18 @@ import { useSaveField } from './useSaveField'; export interface AutosaveAutocompleteProps extends Omit< - AutocompleteProps, + AutocompleteProps, 'renderInput' | 'onChange' | 'value' > { fieldName: string; label: string; textFieldProps?: Partial; + /** + * Option displayed when the field has no saved value, e.g. 'None'. Must be + * one of the options. When set, the field is not clearable — selecting the + * empty-value option takes the place of clearing. + */ + emptyValue?: string; } export const AutosaveAutocomplete: React.FC = ({ @@ -23,16 +29,21 @@ export const AutosaveAutocomplete: React.FC = ({ label, options, textFieldProps, + emptyValue, ...props }) => { const saveField = useSaveField(); const { calculation } = useSalaryCalculator(); - const value = calculation?.[fieldName] ?? null; + const value = calculation?.[fieldName] ?? emptyValue ?? null; return ( saveField({ [fieldName]: newValue })} disabled={!calculation} diff --git a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx index 84988ddd95..a053c8a6cb 100644 --- a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx +++ b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx @@ -12,11 +12,13 @@ const TestComponent: React.FC<{ hasSpouse?: boolean; requestMock?: SalaryRequestMock; payrollDates?: SalaryCalculatorTestWrapperProps['payrollDates']; -}> = ({ hasSpouse, requestMock, payrollDates }) => ( + onCall?: SalaryCalculatorTestWrapperProps['onCall']; +}> = ({ hasSpouse, requestMock, payrollDates, onCall }) => ( @@ -68,10 +70,10 @@ describe('PersonalInformationSection', () => { }); it('should display married personal information values correctly', async () => { - // Explicitly clear the saved location so the combobox is empty. (MUI v7's - // Autocomplete renders a controlled `value` even when it isn't one of the - // `options`, so relying on the auto-generated mock string would populate - // the field.) + // Explicitly clear the saved location so the combobox falls back to None. + // (MUI v7's Autocomplete renders a controlled `value` even when it isn't + // one of the `options`, so relying on the auto-generated mock string would + // populate the field.) const { findByRole } = render( , ); @@ -80,7 +82,7 @@ describe('PersonalInformationSection', () => { name: 'Nearest Geographic Multiplier Location', }); await waitFor(() => { - expect(locationCombobox).toHaveValue(''); + expect(locationCombobox).toHaveValue('None'); }); expect(await findByRole('cell', { name: '4 years' })).toBeInTheDocument(); @@ -112,7 +114,39 @@ describe('PersonalInformationSection', () => { userEvent.click(await findByRole('button', { name: 'Open' })); - expect(await findAllByRole('option')).toHaveLength(2); + // The two mocked locations plus the guaranteed None option + expect(await findAllByRole('option')).toHaveLength(3); + }); + + it('does not save while the location is cleared by typing and reverts on blur', async () => { + const mutationSpy = jest.fn(); + const { findByRole } = render( + , + ); + + const locationCombobox = await findByRole('combobox', { + name: 'Nearest Geographic Multiplier Location', + }); + await waitFor(() => { + expect(locationCombobox).toHaveValue('Miami, FL'); + }); + + userEvent.clear(locationCombobox); + + // Yield to the microtask queue so any pending mutation would have fired. + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdateSalaryCalculation'); + expect(locationCombobox).toHaveValue(''); + + userEvent.tab(); + + // The field is not clearable, so blurring restores the saved value + // without firing a mutation + await waitFor(() => expect(locationCombobox).toHaveValue('Miami, FL')); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdateSalaryCalculation'); }); it('should render the effective paycheck note when payroll dates match', async () => { diff --git a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx index 50e465d86b..8ac9b4a1d7 100644 --- a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx +++ b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx @@ -69,6 +69,7 @@ export const PersonalInformationSection: React.FC = () => { label={t('Nearest Geographic Multiplier Location')} fieldName="location" options={locations} + emptyValue="None" textFieldProps={{ InputLabelProps: { sx: { fontSize: theme.typography.body2.fontSize }, diff --git a/src/hooks/useGoalCalculatorConstants.test.tsx b/src/hooks/useGoalCalculatorConstants.test.tsx index 0806f9d183..8eb124c5f4 100644 --- a/src/hooks/useGoalCalculatorConstants.test.tsx +++ b/src/hooks/useGoalCalculatorConstants.test.tsx @@ -121,7 +121,9 @@ describe('useGoalCalculatorConstants', () => { expect(result.current).toEqual({ goalBenefitsPlans: [], goalMiscConstants: {}, - goalGeographicConstantMap: new Map(), + // The None geographic option is seeded even before the data loads so + // that non-clearable dropdowns always have a valid option + goalGeographicConstantMap: new Map([['None', 0]]), loading: true, error: undefined, unavailable: false, @@ -169,6 +171,46 @@ describe('useGoalCalculatorConstants', () => { expect(result.current.loading).toBe(false); }); + it('guarantees a leading None geographic option when the data lacks one', async () => { + const { result } = renderHook(() => useGoalCalculatorConstants(), { + wrapper: ({ children }: { children: ReactElement }) => ( + + mocks={{ + GoalCalculatorConstants: { + constant: { + ...mockData.constant, + mpdGoalGeographicConstants: [ + { + __typename: 'MpdGoalGeographicConstant' as const, + id: '32818f68-59f7-4a06-83c6-6d286ec29bbf', + location: 'Atlanta, GA', + percentageMultiplier: 0.12, + }, + ], + }, + }, + }} + > + {children} + + ), + }); + + await waitFor(() => + expect(result.current.goalGeographicConstantMap).toEqual( + new Map([ + ['None', 0], + ['Atlanta, GA', 0.12], + ]), + ), + ); + expect(Array.from(result.current.goalGeographicConstantMap.keys())[0]).toBe( + 'None', + ); + }); + it('should format data correctly', async () => { const { result } = renderHook(() => useGoalCalculatorConstants(), { wrapper: ({ children }: { children: ReactElement }) => ( diff --git a/src/hooks/useGoalCalculatorConstants.ts b/src/hooks/useGoalCalculatorConstants.ts index f70c67477a..6b05ff483b 100644 --- a/src/hooks/useGoalCalculatorConstants.ts +++ b/src/hooks/useGoalCalculatorConstants.ts @@ -48,6 +48,8 @@ export const formatConstants = ( }); const goalGeographicConstantMap: GoalGeographicConstantMap = new Map(); + // Ensure the None option always exists + goalGeographicConstantMap.set('None', 0); constant?.mpdGoalGeographicConstants.forEach((constant) => { const { location, percentageMultiplier } = constant; goalGeographicConstantMap.set(location, percentageMultiplier); From 8f4db5989628486681bd2a4987dfdae50cffddb0 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 20 Aug 2026 12:29:13 -0400 Subject: [PATCH 2/6] fix: address review comment on SetupStep.test.tsx:347 Remove redundant first-statement mockClear calls (jest clearMocks is on) Co-Authored-By: Claude Fable 5 --- .../InformationCategory/InformationCategory.test.tsx | 3 --- .../HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx | 4 ---- 2 files changed, 7 deletions(-) diff --git a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx index d544696aca..4e41892abb 100644 --- a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx +++ b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx @@ -328,7 +328,6 @@ describe('InformationCategory', () => { }); it('does not save while the Geographic Location is cleared by typing', async () => { - mutationSpy.mockClear(); const { getByRole } = render( , ); @@ -346,7 +345,6 @@ describe('InformationCategory', () => { }); it('reverts to the saved Geographic Location when the cleared field loses focus', async () => { - mutationSpy.mockClear(); const { getByRole } = render( , ); @@ -364,7 +362,6 @@ describe('InformationCategory', () => { }); it('defaults to None and does not save when cleared and blurred without a saved location', async () => { - mutationSpy.mockClear(); const { getByRole } = render(); const input = getByRole('combobox', { name: 'Geographic Location' }); diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx index d0352c37f7..8a736d7956 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx @@ -345,7 +345,6 @@ describe('SetupStep', () => { }); it('does not save while the Geographic Multiplier is cleared by typing', async () => { - mutationSpy.mockClear(); const { findByRole } = renderSetup({ calculationMock: { ...fullTimeSalariedMock, @@ -369,7 +368,6 @@ describe('SetupStep', () => { }); it('reverts to the saved Geographic Multiplier when the cleared field loses focus', async () => { - mutationSpy.mockClear(); const { findByRole } = renderSetup({ calculationMock: { ...fullTimeSalariedMock, @@ -393,7 +391,6 @@ describe('SetupStep', () => { }); it('saves None when it is explicitly selected', async () => { - mutationSpy.mockClear(); const { findByRole } = renderSetup({ calculationMock: { ...fullTimeSalariedMock, @@ -421,7 +418,6 @@ describe('SetupStep', () => { }); it('does not save when a Geographic Multiplier of None is cleared and loses focus', async () => { - mutationSpy.mockClear(); const { findByRole } = renderSetup({ calculationMock: fullTimeSalariedMock, onCall: mutationSpy, From 28bb676707d8e628cd2a5e24d8a1d16e92a9d91b Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 20 Aug 2026 12:29:40 -0400 Subject: [PATCH 3/6] fix: address review comment on AutosaveAutocomplete.tsx:24 Omit disableClearable so callers cannot re-enable clearing Co-Authored-By: Claude Fable 5 --- .../SalaryCalculator/Autosave/AutosaveAutocomplete.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx b/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx index 69b0b15205..9f02f44ada 100644 --- a/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx +++ b/src/components/HrTools/SalaryCalculator/Autosave/AutosaveAutocomplete.tsx @@ -11,7 +11,10 @@ import { useSaveField } from './useSaveField'; export interface AutosaveAutocompleteProps extends Omit< AutocompleteProps, - 'renderInput' | 'onChange' | 'value' + // disableClearable is owned by the component: it is derived from + // emptyValue, and letting a caller re-enable clearing would restore the + // mid-typing null save this component exists to prevent + 'renderInput' | 'onChange' | 'value' | 'disableClearable' > { fieldName: string; label: string; From aaaf685ddffb4f83fbde86d3c38c570172fadb34 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 20 Aug 2026 12:30:27 -0400 Subject: [PATCH 4/6] fix: address review comment on InformationCategory.test.tsx:330 Add positive save tests for the MPD and Salary geographic fields Co-Authored-By: Claude Fable 5 --- .../InformationCategory.test.tsx | 24 +++++++++++++++++ .../PersonalInformationSection.test.tsx | 26 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx index 4e41892abb..3ad08bb55b 100644 --- a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx +++ b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx @@ -377,6 +377,30 @@ describe('InformationCategory', () => { expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation'); }); + it('saves None when it is explicitly selected', async () => { + const { getByRole, findByRole } = render( + , + ); + + const input = getByRole('combobox', { name: 'Geographic Location' }); + await waitFor(() => expect(input).toHaveValue('Orlando, FL')); + + userEvent.click(input); + userEvent.click(await findByRole('option', { name: 'None' })); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdateGoalCalculation', { + input: { + accountListId: 'account-list-1', + attributes: { + id: 'goal-calculation-1', + geographicLocation: 'None', + }, + }, + }), + ); + }); + it('shows errors and does not save when input is invalid', async () => { const { getByRole } = render(); diff --git a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx index a053c8a6cb..b88afd420a 100644 --- a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx +++ b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx @@ -149,6 +149,32 @@ describe('PersonalInformationSection', () => { expect(mutationSpy).not.toHaveGraphqlOperation('UpdateSalaryCalculation'); }); + it('saves the location when an option is explicitly selected', async () => { + const mutationSpy = jest.fn(); + const { findByRole } = render( + , + ); + + const locationCombobox = await findByRole('combobox', { + name: 'Nearest Geographic Multiplier Location', + }); + await waitFor(() => { + expect(locationCombobox).toHaveValue('Miami, FL'); + }); + + userEvent.click(await findByRole('button', { name: 'Open' })); + userEvent.click(await findByRole('option', { name: 'None' })); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdateSalaryCalculation', { + input: { attributes: { location: 'None' } }, + }), + ); + }); + it('should render the effective paycheck note when payroll dates match', async () => { const { findByRole } = render( Date: Thu, 20 Aug 2026 12:30:46 -0400 Subject: [PATCH 5/6] fix: address review comment on PersonalInformationSection.test.tsx:121 Assert a server None row overwrites the seeded value without moving it Co-Authored-By: Claude Fable 5 --- src/hooks/useGoalCalculatorConstants.test.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/hooks/useGoalCalculatorConstants.test.tsx b/src/hooks/useGoalCalculatorConstants.test.tsx index 8eb124c5f4..75c7b8858e 100644 --- a/src/hooks/useGoalCalculatorConstants.test.tsx +++ b/src/hooks/useGoalCalculatorConstants.test.tsx @@ -211,6 +211,50 @@ describe('useGoalCalculatorConstants', () => { ); }); + it('lets a server None row overwrite the seeded value without moving it', async () => { + const { result } = renderHook(() => useGoalCalculatorConstants(), { + wrapper: ({ children }: { children: ReactElement }) => ( + + mocks={{ + GoalCalculatorConstants: { + constant: { + ...mockData.constant, + mpdGoalGeographicConstants: [ + { + __typename: 'MpdGoalGeographicConstant' as const, + id: '32818f68-59f7-4a06-83c6-6d286ec29bbf', + location: 'Atlanta, GA', + percentageMultiplier: 0.12, + }, + { + __typename: 'MpdGoalGeographicConstant' as const, + id: 'd1097a97-2a16-4c48-9ab5-5121d8ca129e', + location: 'None', + percentageMultiplier: 0.05, + }, + ], + }, + }, + }} + > + {children} + + ), + }); + + // The server's multiplier wins over the seeded 0 even though its row + // arrives second, and None keeps the leading position from the seed + await waitFor(() => + expect(result.current.goalGeographicConstantMap.get('None')).toBe(0.05), + ); + expect(Array.from(result.current.goalGeographicConstantMap.keys())).toEqual([ + 'None', + 'Atlanta, GA', + ]); + }); + it('should format data correctly', async () => { const { result } = renderHook(() => useGoalCalculatorConstants(), { wrapper: ({ children }: { children: ReactElement }) => ( From fb34cc3af423ee6c7fbfcf65ba4cff593e494c12 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 20 Aug 2026 12:33:06 -0400 Subject: [PATCH 6/6] fix: address review comment on useGoalCalculatorConstants.ts:56 Export GEOGRAPHIC_LOCATION_NONE and use it at every production site Co-Authored-By: Claude Fable 5 --- .../InformationCategoryPersonalForm.tsx | 3 ++- .../HrTools/PdsGoalCalculator/Setup/SetupStep.tsx | 9 +++++++-- .../PersonalInformationSection.tsx | 7 +++++-- src/hooks/useGoalCalculatorConstants.test.tsx | 7 +++---- src/hooks/useGoalCalculatorConstants.ts | 10 +++++++++- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx index cde5e0b36f..dfbe6f6792 100644 --- a/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx +++ b/src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx @@ -18,6 +18,7 @@ import { MpdGoalBenefitsConstantPlanEnum, MpdGoalBenefitsConstantSizeEnum, } from 'src/graphql/types.generated'; +import { GEOGRAPHIC_LOCATION_NONE } from 'src/hooks/useGoalCalculatorConstants'; import { getLocalizedAge } from 'src/lib/functions/getLocalizedAge'; import { getLocalizedRole } from 'src/lib/functions/getLocalizedRole'; import { AutosaveTextField } from '../../Autosave/AutosaveTextField'; @@ -153,7 +154,7 @@ export const InformationCategoryPersonalForm: React.FC< // The None option takes the place of clearing the field, so // emptying the input never fires a mid-typing null save disableClearable - value={geographicLocation ?? 'None'} + value={geographicLocation ?? GEOGRAPHIC_LOCATION_NONE} onChange={(_, newValue) => saveField({ geographicLocation: newValue }) } diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx index 935a0eab31..592b79040d 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx @@ -27,7 +27,10 @@ import { DesignationSupportSalaryType, DesignationSupportStatus, } from 'src/graphql/types.generated'; -import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants'; +import { + GEOGRAPHIC_LOCATION_NONE, + useGoalCalculatorConstants, +} from 'src/hooks/useGoalCalculatorConstants'; import { useLocale } from 'src/hooks/useLocale'; import { percentageFormat } from 'src/lib/intlFormat'; import { AutosaveTextField } from '../Shared/Autosave/AutosaveTextField'; @@ -285,7 +288,9 @@ export const SetupStep: React.FC = () => { // emptying the input never fires a mid-typing null save disableClearable getOptionLabel={getLocationLabel} - value={calculation?.geographicLocation ?? 'None'} + value={ + calculation?.geographicLocation ?? GEOGRAPHIC_LOCATION_NONE + } onChange={(_, newValue) => saveField({ geographicLocation: newValue }) } diff --git a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx index 8ac9b4a1d7..a275b8d14b 100644 --- a/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx +++ b/src/components/HrTools/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.tsx @@ -11,7 +11,10 @@ import { } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { Trans, useTranslation } from 'react-i18next'; -import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants'; +import { + GEOGRAPHIC_LOCATION_NONE, + useGoalCalculatorConstants, +} from 'src/hooks/useGoalCalculatorConstants'; import { AutosaveAutocomplete } from '../../Autosave/AutosaveAutocomplete'; import { useSalaryCalculator } from '../../SalaryCalculatorContext/SalaryCalculatorContext'; import { EffectiveDateNote } from '../../Shared/EffectiveDateNote'; @@ -69,7 +72,7 @@ export const PersonalInformationSection: React.FC = () => { label={t('Nearest Geographic Multiplier Location')} fieldName="location" options={locations} - emptyValue="None" + emptyValue={GEOGRAPHIC_LOCATION_NONE} textFieldProps={{ InputLabelProps: { sx: { fontSize: theme.typography.body2.fontSize }, diff --git a/src/hooks/useGoalCalculatorConstants.test.tsx b/src/hooks/useGoalCalculatorConstants.test.tsx index 75c7b8858e..ebc2977188 100644 --- a/src/hooks/useGoalCalculatorConstants.test.tsx +++ b/src/hooks/useGoalCalculatorConstants.test.tsx @@ -249,10 +249,9 @@ describe('useGoalCalculatorConstants', () => { await waitFor(() => expect(result.current.goalGeographicConstantMap.get('None')).toBe(0.05), ); - expect(Array.from(result.current.goalGeographicConstantMap.keys())).toEqual([ - 'None', - 'Atlanta, GA', - ]); + expect(Array.from(result.current.goalGeographicConstantMap.keys())).toEqual( + ['None', 'Atlanta, GA'], + ); }); it('should format data correctly', async () => { diff --git a/src/hooks/useGoalCalculatorConstants.ts b/src/hooks/useGoalCalculatorConstants.ts index 6b05ff483b..13d35ef3d3 100644 --- a/src/hooks/useGoalCalculatorConstants.ts +++ b/src/hooks/useGoalCalculatorConstants.ts @@ -31,6 +31,14 @@ export type GoalMiscConstants = Partial< >; export type GoalGeographicConstantMap = Map; +/** + * The zero-multiplier geographic option meaning "no city applies". Must + * byte-match the server's constants row: the dropdowns are not clearable, so + * this exact string is both the guaranteed option and the value saved when a + * user declines a city. + */ +export const GEOGRAPHIC_LOCATION_NONE = 'None'; + export interface FormattedConstants { goalBenefitsPlans: GoalCalculatorConstantsQuery['constant']['mpdGoalBenefitsConstants']; goalMiscConstants: GoalMiscConstants; @@ -49,7 +57,7 @@ export const formatConstants = ( const goalGeographicConstantMap: GoalGeographicConstantMap = new Map(); // Ensure the None option always exists - goalGeographicConstantMap.set('None', 0); + goalGeographicConstantMap.set(GEOGRAPHIC_LOCATION_NONE, 0); constant?.mpdGoalGeographicConstants.forEach((constant) => { const { location, percentageMultiplier } = constant; goalGeographicConstantMap.set(location, percentageMultiplier);