From a53db3a86de76f7effa86c3309326164b5d3259e Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 8 May 2026 16:48:44 -0400 Subject: [PATCH 01/11] [MPDX-9540] - Show step progress and current step indicator in PDS Goal Calculator Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Shared/PdsGoalCalculatorContext.tsx | 16 ++++- .../Shared/PdsGoalCalculatorLayout.tsx | 3 +- .../Shared/useSteps.test.tsx | 23 +++++++ .../PdsGoalCalculator/Shared/useSteps.tsx | 61 +++++++++++++++---- 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index 552243cb9a..63c898c0a8 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -34,6 +34,7 @@ export type PdsGoalCalculatorType = { calculationLoading: boolean; hcmUser?: HcmUserQuery['hcm'][number]; summaryData: PdsSummaryData | null; + percentComplete: number; /** Whether any mutations are currently in progress */ isMutating: boolean; @@ -89,9 +90,6 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const summaryData = usePdsSummaryData(calculation, hcmUser); - const steps = useSteps( - calculation?.formType ?? DesignationSupportFormType.Detailed, - ); // Track the user's place by step enum, not numeric index, so that a change // to the steps array (e.g. formType switch Detailed → Simple, dropping the // ReimbursableExpenses step) preserves their step when it still exists. @@ -100,6 +98,11 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const [activeStep, setActiveStep] = useState( PdsGoalCalculatorStepEnum.Setup, ); + + const steps = useSteps( + calculation?.formType ?? DesignationSupportFormType.Detailed, + activeStep, + ); const [rightPanelContent, setRightPanelContent] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(true); @@ -125,6 +128,11 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { // protects against an out-of-range stepIndex. const currentStep = steps[stepIndex] ?? steps[0]; + const percentComplete = useMemo( + () => Math.round(((stepIndex + 1) / steps.length) * 100), + [stepIndex, steps.length], + ); + const handleStepChange = useCallback( (newStep: PdsGoalCalculatorStepEnum) => { if (steps.some((step) => step.step === newStep)) { @@ -166,6 +174,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { calculation, calculationLoading, summaryData, + percentComplete, isMutating, trackMutation, hcmUser, @@ -186,6 +195,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { calculation, calculationLoading, summaryData, + percentComplete, isMutating, trackMutation, hcmUser, diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx index 818af71cd9..888926bcbd 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx @@ -23,6 +23,7 @@ export const PdsGoalCalculatorLayout: React.FC< isDrawerOpen, setDrawerOpen, toggleDrawer, + percentComplete, } = usePdsGoalCalculator(); const handleStepIconClick = (step: PdsGoalCalculatorStepEnum) => { @@ -45,7 +46,7 @@ export const PdsGoalCalculatorLayout: React.FC< return ( { PdsGoalCalculatorStepEnum.SummaryReport, ]); }); + + it('marks the active step and prior steps as complete', () => { + const { result } = renderHook(() => + useSteps( + DesignationSupportFormType.Detailed, + PdsGoalCalculatorStepEnum.SupportItem, + ), + ); + expect( + result.current.map((step) => step.sections.every((s) => s.complete)), + ).toEqual([true, true, true, false]); + }); + + it('leaves all sections incomplete when no active step is provided', () => { + const { result } = renderHook(() => + useSteps(DesignationSupportFormType.Detailed), + ); + expect( + result.current.flatMap((step) => + step.sections.map((s) => s.complete), + ), + ).toEqual([false, false, false, false, false, false]); + }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx index 52fa7d7245..718435d6e4 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx @@ -27,45 +27,84 @@ export type PdsGoalCalculatorSteps = [ export const useSteps = ( formType: DesignationSupportFormType, + activeStep?: PdsGoalCalculatorStepEnum, ): PdsGoalCalculatorSteps => { const { t } = useTranslation(); return useMemo(() => { const isSimple = isSimpleFormType(formType); + const buildSections = ( + stepIndex: number, + activeIndex: number, + titles: string[], + ): PdsGoalCalculatorSection[] => + titles.map((title) => ({ + title, + complete: activeIndex >= 0 && stepIndex <= activeIndex, + })); + + const orderedKeys: PdsGoalCalculatorStepEnum[] = isSimple + ? [ + PdsGoalCalculatorStepEnum.Setup, + PdsGoalCalculatorStepEnum.SupportItem, + PdsGoalCalculatorStepEnum.SummaryReport, + ] + : [ + PdsGoalCalculatorStepEnum.Setup, + PdsGoalCalculatorStepEnum.ReimbursableExpenses, + PdsGoalCalculatorStepEnum.SupportItem, + PdsGoalCalculatorStepEnum.SummaryReport, + ]; + + const activeIndex = activeStep ? orderedKeys.indexOf(activeStep) : -1; + const setup: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.Setup, title: t('Settings'), icon: , - sections: [{ title: t('Setup'), complete: false }], + sections: buildSections( + orderedKeys.indexOf(PdsGoalCalculatorStepEnum.Setup), + activeIndex, + [t('Setup')], + ), }; const reimbursableExpenses: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.ReimbursableExpenses, title: t('Reimbursable Expenses'), icon: , - sections: [ - { title: t('Monthly Reimbursable Expenses'), complete: false }, - { title: t('Annual Reimbursable Expenses'), complete: false }, - ], + sections: buildSections( + orderedKeys.indexOf(PdsGoalCalculatorStepEnum.ReimbursableExpenses), + activeIndex, + [ + t('Monthly Reimbursable Expenses'), + t('Annual Reimbursable Expenses'), + ], + ), }; const supportItem: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.SupportItem, title: t('Support Item'), icon: , - sections: [ - { title: t('Salary'), complete: false }, - { title: t('Other'), complete: false }, - ], + sections: buildSections( + orderedKeys.indexOf(PdsGoalCalculatorStepEnum.SupportItem), + activeIndex, + [t('Salary'), t('Other')], + ), }; const summaryReport: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.SummaryReport, title: t('Summary Report'), icon: , - sections: [{ title: t('MPD Goal'), complete: false }], + sections: buildSections( + orderedKeys.indexOf(PdsGoalCalculatorStepEnum.SummaryReport), + activeIndex, + [t('MPD Goal')], + ), }; return isSimple ? [setup, supportItem, summaryReport] : [setup, reimbursableExpenses, supportItem, summaryReport]; - }, [t, formType]); + }, [t, formType, activeStep]); }; From 1517e858f66204fcd4e228bd7d68da3e350a7b73 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 8 May 2026 16:53:29 -0400 Subject: [PATCH 02/11] Runs prettier --- .../HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx | 4 +--- src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx index f2e973c472..1491da5f4b 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx @@ -44,9 +44,7 @@ describe('useSteps', () => { useSteps(DesignationSupportFormType.Detailed), ); expect( - result.current.flatMap((step) => - step.sections.map((s) => s.complete), - ), + result.current.flatMap((step) => step.sections.map((s) => s.complete)), ).toEqual([false, false, false, false, false, false]); }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx index 718435d6e4..19d4afbc54 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx @@ -76,10 +76,7 @@ export const useSteps = ( sections: buildSections( orderedKeys.indexOf(PdsGoalCalculatorStepEnum.ReimbursableExpenses), activeIndex, - [ - t('Monthly Reimbursable Expenses'), - t('Annual Reimbursable Expenses'), - ], + [t('Monthly Reimbursable Expenses'), t('Annual Reimbursable Expenses')], ), }; const supportItem: PdsGoalCalculatorStep = { From 3af3f75a6c29cccdb8c1bae7d07d5aeb974ea81e Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 13:14:24 -0400 Subject: [PATCH 03/11] Fix completion logic --- .../PdsGoalCalculator/PdsGoalCalculator.tsx | 25 ++- .../ReimbursableExpensesSectionList.tsx | 28 ++++ .../Setup/SetupSectionList.tsx | 16 ++ .../Shared/PdsGoalCalculatorContext.test.tsx | 1 - .../Shared/PdsGoalCalculatorContext.tsx | 1 - .../Shared/pdsCompletion.test.ts | 143 ++++++++++++++++++ .../PdsGoalCalculator/Shared/pdsCompletion.ts | 71 +++++++++ .../Shared/useSteps.test.tsx | 21 --- .../PdsGoalCalculator/Shared/useSteps.tsx | 58 +------ .../SummaryReportSectionList.tsx | 18 +++ .../SupportItem/SupportItemSectionList.tsx | 19 +++ 11 files changed, 317 insertions(+), 84 deletions(-) create mode 100644 src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts create mode 100644 src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts create mode 100644 src/components/HrTools/PdsGoalCalculator/SummaryReport/SummaryReportSectionList.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.tsx index f3aba67f33..2cbb3403cd 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.tsx @@ -4,13 +4,16 @@ import { AutosaveForm, useAutosaveForm, } from 'src/components/Shared/Autosave/AutosaveForm'; -import { SectionList } from '../GoalCalculator/SharedComponents/SectionList'; import { PdsGoalCalculatorStepEnum } from './PdsGoalCalculatorHelper'; +import { ReimbursableExpensesSectionList } from './ReimbursableExpenses/ReimbursableExpensesSectionList'; import { ReimbursableExpensesStep } from './ReimbursableExpenses/ReimbursableExpensesStep'; +import { SetupSectionList } from './Setup/SetupSectionList'; import { SetupStep } from './Setup/SetupStep'; import { usePdsGoalCalculator } from './Shared/PdsGoalCalculatorContext'; import { PdsGoalCalculatorLayout } from './Shared/PdsGoalCalculatorLayout'; +import { SummaryReportSectionList } from './SummaryReport/SummaryReportSectionList'; import { SummaryReportStep } from './SummaryReport/SummaryReportStep'; +import { SupportItemSectionList } from './SupportItem/SupportItemSectionList'; import { SupportItemStep } from './SupportItem/SupportItemStep'; const CurrentStep: React.FC = () => { @@ -28,6 +31,21 @@ const CurrentStep: React.FC = () => { } }; +const CurrentSectionList: React.FC = () => { + const { currentStep } = usePdsGoalCalculator(); + + switch (currentStep.step) { + case PdsGoalCalculatorStepEnum.Setup: + return ; + case PdsGoalCalculatorStepEnum.ReimbursableExpenses: + return ; + case PdsGoalCalculatorStepEnum.SupportItem: + return ; + case PdsGoalCalculatorStepEnum.SummaryReport: + return ; + } +}; + const MainContent: React.FC = () => { const { currentStep, stepIndex, steps, handleContinue, handlePreviousStep } = usePdsGoalCalculator(); @@ -52,12 +70,9 @@ const MainContent: React.FC = () => { }; export const PdsGoalCalculator: React.FC = () => { - const { currentStep } = usePdsGoalCalculator(); - const sections = currentStep.sections; - return ( } + sectionListPanel={} mainContent={ diff --git a/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.tsx b/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.tsx new file mode 100644 index 0000000000..a37b1da61f --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SectionList } from '../../GoalCalculator/SharedComponents/SectionList'; +import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; +import { + isAnnualReimbursableComplete, + isMonthlyReimbursableComplete, +} from '../Shared/pdsCompletion'; + +export const ReimbursableExpensesSectionList: React.FC = () => { + const { t } = useTranslation(); + const { calculation } = usePdsGoalCalculator(); + + return ( + + ); +}; diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.tsx new file mode 100644 index 0000000000..167d91a3d0 --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SectionList } from '../../GoalCalculator/SharedComponents/SectionList'; +import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; +import { isSetupComplete } from '../Shared/pdsCompletion'; + +export const SetupSectionList: React.FC = () => { + const { t } = useTranslation(); + const { calculation } = usePdsGoalCalculator(); + + return ( + + ); +}; diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index 3177c0a424..944754ff74 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -24,7 +24,6 @@ const stub = (step: PdsGoalCalculatorStepEnum): PdsGoalCalculatorStep => ({ step, title: step, icon: , - sections: [], }); const detailedSteps: PdsGoalCalculatorSteps = [ diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index 63c898c0a8..640ff8c212 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -101,7 +101,6 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const steps = useSteps( calculation?.formType ?? DesignationSupportFormType.Detailed, - activeStep, ); const [rightPanelContent, setRightPanelContent] = useState(null); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts new file mode 100644 index 0000000000..f4affa1307 --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts @@ -0,0 +1,143 @@ +import { + DesignationSupportSalaryType, + DesignationSupportStatus, +} from 'src/graphql/types.generated'; +import { PdsGoalCalculationFieldsFragment } from '../GoalsList/PdsGoalCalculations.generated'; +import { PdsSummaryData } from '../calculations/usePdsSummaryData'; +import { + isAnnualReimbursableComplete, + isMonthlyReimbursableComplete, + isMpdGoalComplete, + isOtherComplete, + isSalaryComplete, + isSetupComplete, +} from './pdsCompletion'; + +const baseCalculation: PdsGoalCalculationFieldsFragment = { + id: 'goal-1', + name: 'Test Goal', + status: DesignationSupportStatus.FullTime, + salaryOrHourly: DesignationSupportSalaryType.Salaried, + payRate: 50000, + hoursWorkedPerWeek: null, + benefits: 1500, + formType: null, + updatedAt: '2026-01-01T00:00:00Z', + averageHoursPerWeek: null, + geographicLocation: null, + ministryCellPhone: null, + ministryInternet: null, + mpdNewsletter: null, + mpdMiscellaneous: null, + accountTransfers: null, + otherMonthlyReimbursements: null, + conferenceRetreatCosts: null, + ministryTravelMeals: null, + otherAnnualReimbursements: null, + designationSupportHoursItems: [], +}; + +describe('isSetupComplete', () => { + it('returns false when calculation is undefined', () => { + expect(isSetupComplete(undefined)).toBe(false); + }); + + it('returns true when all required fields are set for a salaried full-time goal', () => { + expect(isSetupComplete(baseCalculation)).toBe(true); + }); + + it('returns false when name is empty', () => { + expect(isSetupComplete({ ...baseCalculation, name: '' })).toBe(false); + }); + + it('returns false when payRate is missing or zero', () => { + expect(isSetupComplete({ ...baseCalculation, payRate: null })).toBe(false); + expect(isSetupComplete({ ...baseCalculation, payRate: 0 })).toBe(false); + }); + + it('requires hoursWorkedPerWeek when pay type is Hourly', () => { + const hourly: PdsGoalCalculationFieldsFragment = { + ...baseCalculation, + salaryOrHourly: DesignationSupportSalaryType.Hourly, + hoursWorkedPerWeek: null, + }; + expect(isSetupComplete(hourly)).toBe(false); + expect(isSetupComplete({ ...hourly, hoursWorkedPerWeek: 30 })).toBe(true); + }); + + it('does not require benefits when status is Part-time', () => { + const partTime: PdsGoalCalculationFieldsFragment = { + ...baseCalculation, + status: DesignationSupportStatus.PartTime, + benefits: null, + }; + expect(isSetupComplete(partTime)).toBe(true); + }); + + it('requires benefits when status is Full-time', () => { + expect(isSetupComplete({ ...baseCalculation, benefits: null })).toBe(false); + }); +}); + +describe('isMonthlyReimbursableComplete', () => { + it('returns false when no monthly fields have been touched', () => { + expect(isMonthlyReimbursableComplete(baseCalculation)).toBe(false); + }); + + it('returns true when any monthly field has a value, including zero', () => { + expect( + isMonthlyReimbursableComplete({ + ...baseCalculation, + ministryCellPhone: 0, + }), + ).toBe(true); + expect( + isMonthlyReimbursableComplete({ + ...baseCalculation, + otherMonthlyReimbursements: 25, + }), + ).toBe(true); + }); +}); + +describe('isAnnualReimbursableComplete', () => { + it('returns false when no annual fields have been touched', () => { + expect(isAnnualReimbursableComplete(baseCalculation)).toBe(false); + }); + + it('returns true when any annual field has a value', () => { + expect( + isAnnualReimbursableComplete({ + ...baseCalculation, + conferenceRetreatCosts: 0, + }), + ).toBe(true); + }); +}); + +describe('isSalaryComplete / isOtherComplete', () => { + it('mirror isSetupComplete', () => { + expect(isSalaryComplete(baseCalculation)).toBe(true); + expect(isOtherComplete(baseCalculation)).toBe(true); + expect(isSalaryComplete({ ...baseCalculation, payRate: null })).toBe(false); + expect(isOtherComplete({ ...baseCalculation, payRate: null })).toBe(false); + }); +}); + +describe('isMpdGoalComplete', () => { + it('returns false when summaryData is null', () => { + expect(isMpdGoalComplete(null)).toBe(false); + }); + + it('returns false when overallTotal is zero', () => { + expect(isMpdGoalComplete({ overallTotal: 0 } as PdsSummaryData)).toBe( + false, + ); + }); + + it('returns true when overallTotal is positive', () => { + expect(isMpdGoalComplete({ overallTotal: 1234 } as PdsSummaryData)).toBe( + true, + ); + }); +}); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts new file mode 100644 index 0000000000..44a7473ce6 --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts @@ -0,0 +1,71 @@ +import { + DesignationSupportSalaryType, + DesignationSupportStatus, +} from 'src/graphql/types.generated'; +import { PdsGoalCalculationFieldsFragment } from '../GoalsList/PdsGoalCalculations.generated'; +import { PdsSummaryData } from '../calculations/usePdsSummaryData'; + +type Calculation = PdsGoalCalculationFieldsFragment | undefined; + +export const isSetupComplete = (calculation: Calculation): boolean => { + if (!calculation) { + return false; + } + + const isSalaried = + calculation.salaryOrHourly === DesignationSupportSalaryType.Salaried; + const isPartTime = calculation.status === DesignationSupportStatus.PartTime; + + return Boolean( + calculation.name && + calculation.status && + calculation.salaryOrHourly && + (calculation.payRate ?? 0) > 0 && + (isSalaried || (calculation.hoursWorkedPerWeek ?? 0) > 0) && + (isPartTime || (calculation.benefits ?? 0) > 0), + ); +}; + +// Cells default to null and only become a number when the user types one, +// so we can distinguish "user engaged with this section" from a fresh state. +const isAnyFieldTouched = ( + values: ReadonlyArray, +): boolean => values.some((value) => value !== null && value !== undefined); + +export const isMonthlyReimbursableComplete = ( + calculation: Calculation, +): boolean => { + if (!calculation) { + return false; + } + return isAnyFieldTouched([ + calculation.ministryCellPhone, + calculation.ministryInternet, + calculation.mpdNewsletter, + calculation.mpdMiscellaneous, + calculation.accountTransfers, + calculation.otherMonthlyReimbursements, + ]); +}; + +export const isAnnualReimbursableComplete = ( + calculation: Calculation, +): boolean => { + if (!calculation) { + return false; + } + return isAnyFieldTouched([ + calculation.conferenceRetreatCosts, + calculation.ministryTravelMeals, + calculation.otherAnnualReimbursements, + ]); +}; + +// Salary and Other are read-only derived tables whose only required inputs +// come from the Setup step. +export const isSalaryComplete = isSetupComplete; +export const isOtherComplete = isSetupComplete; + +export const isMpdGoalComplete = ( + summaryData: PdsSummaryData | null, +): boolean => !!summaryData && summaryData.overallTotal > 0; diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx index 1491da5f4b..72ff2ea48d 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx @@ -26,25 +26,4 @@ describe('useSteps', () => { PdsGoalCalculatorStepEnum.SummaryReport, ]); }); - - it('marks the active step and prior steps as complete', () => { - const { result } = renderHook(() => - useSteps( - DesignationSupportFormType.Detailed, - PdsGoalCalculatorStepEnum.SupportItem, - ), - ); - expect( - result.current.map((step) => step.sections.every((s) => s.complete)), - ).toEqual([true, true, true, false]); - }); - - it('leaves all sections incomplete when no active step is provided', () => { - const { result } = renderHook(() => - useSteps(DesignationSupportFormType.Detailed), - ); - expect( - result.current.flatMap((step) => step.sections.map((s) => s.complete)), - ).toEqual([false, false, false, false, false, false]); - }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx index 19d4afbc54..c87380e0d4 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx @@ -8,16 +8,10 @@ import { DesignationSupportFormType } from 'src/graphql/types.generated'; import { PdsGoalCalculatorStepEnum } from '../PdsGoalCalculatorHelper'; import { isSimpleFormType } from './formType'; -export interface PdsGoalCalculatorSection { - title: string; - complete: boolean; -} - export interface PdsGoalCalculatorStep { step: PdsGoalCalculatorStepEnum; title: string; icon: React.ReactNode; - sections: PdsGoalCalculatorSection[]; } export type PdsGoalCalculatorSteps = [ @@ -27,81 +21,33 @@ export type PdsGoalCalculatorSteps = [ export const useSteps = ( formType: DesignationSupportFormType, - activeStep?: PdsGoalCalculatorStepEnum, ): PdsGoalCalculatorSteps => { const { t } = useTranslation(); return useMemo(() => { - const isSimple = isSimpleFormType(formType); - - const buildSections = ( - stepIndex: number, - activeIndex: number, - titles: string[], - ): PdsGoalCalculatorSection[] => - titles.map((title) => ({ - title, - complete: activeIndex >= 0 && stepIndex <= activeIndex, - })); - - const orderedKeys: PdsGoalCalculatorStepEnum[] = isSimple - ? [ - PdsGoalCalculatorStepEnum.Setup, - PdsGoalCalculatorStepEnum.SupportItem, - PdsGoalCalculatorStepEnum.SummaryReport, - ] - : [ - PdsGoalCalculatorStepEnum.Setup, - PdsGoalCalculatorStepEnum.ReimbursableExpenses, - PdsGoalCalculatorStepEnum.SupportItem, - PdsGoalCalculatorStepEnum.SummaryReport, - ]; - - const activeIndex = activeStep ? orderedKeys.indexOf(activeStep) : -1; - const setup: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.Setup, title: t('Settings'), icon: , - sections: buildSections( - orderedKeys.indexOf(PdsGoalCalculatorStepEnum.Setup), - activeIndex, - [t('Setup')], - ), }; const reimbursableExpenses: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.ReimbursableExpenses, title: t('Reimbursable Expenses'), icon: , - sections: buildSections( - orderedKeys.indexOf(PdsGoalCalculatorStepEnum.ReimbursableExpenses), - activeIndex, - [t('Monthly Reimbursable Expenses'), t('Annual Reimbursable Expenses')], - ), }; const supportItem: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.SupportItem, title: t('Support Item'), icon: , - sections: buildSections( - orderedKeys.indexOf(PdsGoalCalculatorStepEnum.SupportItem), - activeIndex, - [t('Salary'), t('Other')], - ), }; const summaryReport: PdsGoalCalculatorStep = { step: PdsGoalCalculatorStepEnum.SummaryReport, title: t('Summary Report'), icon: , - sections: buildSections( - orderedKeys.indexOf(PdsGoalCalculatorStepEnum.SummaryReport), - activeIndex, - [t('MPD Goal')], - ), }; - return isSimple + return isSimpleFormType(formType) ? [setup, supportItem, summaryReport] : [setup, reimbursableExpenses, supportItem, summaryReport]; - }, [t, formType, activeStep]); + }, [t, formType]); }; diff --git a/src/components/HrTools/PdsGoalCalculator/SummaryReport/SummaryReportSectionList.tsx b/src/components/HrTools/PdsGoalCalculator/SummaryReport/SummaryReportSectionList.tsx new file mode 100644 index 0000000000..b0f2593c7e --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/SummaryReport/SummaryReportSectionList.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SectionList } from '../../GoalCalculator/SharedComponents/SectionList'; +import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; +import { isMpdGoalComplete } from '../Shared/pdsCompletion'; + +export const SummaryReportSectionList: React.FC = () => { + const { t } = useTranslation(); + const { summaryData } = usePdsGoalCalculator(); + + return ( + + ); +}; diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx new file mode 100644 index 0000000000..7c88a86e0f --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SectionList } from '../../GoalCalculator/SharedComponents/SectionList'; +import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; +import { isOtherComplete, isSalaryComplete } from '../Shared/pdsCompletion'; + +export const SupportItemSectionList: React.FC = () => { + const { t } = useTranslation(); + const { calculation } = usePdsGoalCalculator(); + + return ( + + ); +}; From 51cadfc8b73d1e92496f33a65cd0cc8527ccc298 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 13:18:02 -0400 Subject: [PATCH 04/11] Fix test --- .../Shared/PdsGoalCalculatorContext.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index 944754ff74..4141a90223 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -144,6 +144,18 @@ describe('PdsGoalCalculatorContext', () => { expect(result.current.stepIndex).toBe(0); }); + it('exposes percentComplete reflecting current step / total steps', () => { + const { result } = renderUsePdsGoalCalculator(); + + expect(result.current.percentComplete).toBe(25); + act(() => result.current.handleContinue()); + expect(result.current.percentComplete).toBe(50); + act(() => result.current.handleContinue()); + expect(result.current.percentComplete).toBe(75); + act(() => result.current.handleContinue()); + expect(result.current.percentComplete).toBe(100); + }); + describe('preserves the user step when the steps array changes', () => { const reconcileMessage = 'Returned to Setup because the current step is no longer available.'; From 50f99de0ccfc8f352a38362a85c3d794165ff474 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 14:26:25 -0400 Subject: [PATCH 05/11] PR fixes --- .../SharedComponents/SectionList.tsx | 54 ++++++++++----- .../Shared/PdsGoalCalculatorContext.test.tsx | 66 +++++++++++++++++++ .../Shared/PdsGoalCalculatorLayout.test.tsx | 16 +++++ .../Shared/PdsGoalCalculatorLayout.tsx | 2 + .../Shared/useSteps.test.tsx | 58 ++++++++++++++++ .../CircularProgressWithLabel.tsx | 7 +- .../PanelLayout/PanelLayout.tsx | 1 + 7 files changed, 185 insertions(+), 19 deletions(-) diff --git a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.tsx b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.tsx index 0c5470e11e..08c9524db8 100644 --- a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.tsx +++ b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.tsx @@ -13,28 +13,42 @@ import { useGoalCalculator } from '../Shared/GoalCalculatorContext'; interface ListItemContentProps { title: string; complete: boolean; + announceCompletion?: boolean; } const ListItemContent: React.FC = ({ title, complete, -}) => ( - <> - ({ - color: complete - ? theme.palette.mpdxBlue.main - : theme.palette.mpdxGrayDark.main, - })} - > - {complete ? : } - - - -); + announceCompletion = false, +}) => { + const { t } = useTranslation(); + const titleAccess = announceCompletion + ? complete + ? t('Complete') + : t('Incomplete') + : undefined; + return ( + <> + ({ + color: complete + ? theme.palette.mpdxBlue.main + : theme.palette.mpdxGrayDark.main, + })} + > + {complete ? ( + + ) : ( + + )} + + + + ); +}; export interface SectionItem { title: string; @@ -50,7 +64,11 @@ export const SectionList: React.FC = ({ sections }) => { {sections.map(({ title, complete }, index) => ( - + ))} diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index 4141a90223..c1e3b83357 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -238,6 +238,72 @@ describe('PdsGoalCalculatorContext', () => { }, ); + it('falls back safely on the one-frame transient render when activeStep is foreign to the new steps array', async () => { + const renderLog: Array<{ + step: PdsGoalCalculatorStepEnum; + index: number; + percent: number; + stepCount: number; + }> = []; + + const RecordingProbe: React.FC = () => { + const { currentStep, stepIndex, percentComplete, steps, handleStepChange } = + usePdsGoalCalculator(); + renderLog.push({ + step: currentStep.step, + index: stepIndex, + percent: percentComplete, + stepCount: steps.length, + }); + return ( + + ); + }; + + mockedUseSteps.mockReturnValue(detailedSteps); + const { getByRole, rerender } = render( + + + , + ); + + userEvent.click(getByRole('button', { name: 'go to reimbursable' })); + + const renderCountBeforeSwitch = renderLog.length; + + mockedUseSteps.mockReturnValue(simpleSteps); + rerender( + + + , + ); + + // The rerender produces a transient render (steps=simple, activeStep still + // ReimbursableExpenses) followed by a reconciled render after the effect + // calls setActiveStep(Setup). Pin both. + const transient = renderLog[renderCountBeforeSwitch]; + expect(transient.stepCount).toBe(3); + // findIndex returns -1 here; the `=== -1 ? 0` fallback at stepIndex memo + // and the `?? steps[0]` fallback on currentStep keep this render safe. + expect(transient.index).toBe(0); + expect(transient.step).toBe(PdsGoalCalculatorStepEnum.Setup); + // Without the -1 fallback, percentComplete would be 0% (Math.round(0/3)); + // pinning 33% guards against a regression that drops the guard. + expect(transient.percent).toBe(33); + + const reconciled = renderLog[renderLog.length - 1]; + expect(reconciled.step).toBe(PdsGoalCalculatorStepEnum.Setup); + expect(reconciled.index).toBe(0); + expect(reconciled.percent).toBe(33); + }); + it('reconciles activeStep state so toggling formType back does not teleport the user', async () => { mockedUseSteps.mockReturnValue(detailedSteps); const { findByTestId, getByRole, rerender } = render( diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx index 4429ac7282..60e643f126 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx @@ -16,4 +16,20 @@ describe('PdsGoalCalculatorLayout', () => { expect(getByText('Main Content')).toBeInTheDocument(); }); + + it('passes percentComplete from context to the progress indicator', async () => { + // Detailed formType yields 4 steps; the initial Setup step is index 0, + // so percentComplete = round((0 + 1) / 4 * 100) = 25. + const { findByRole } = render( + + Section List} + mainContent={
Main Content
} + /> +
, + ); + + const progressIndicator = await findByRole('progressbar'); + expect(progressIndicator).toHaveAttribute('aria-valuenow', '25'); + }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx index 888926bcbd..e2034a72b9 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx @@ -24,6 +24,7 @@ export const PdsGoalCalculatorLayout: React.FC< setDrawerOpen, toggleDrawer, percentComplete, + calculationLoading, } = usePdsGoalCalculator(); const handleStepIconClick = (step: PdsGoalCalculatorStepEnum) => { @@ -47,6 +48,7 @@ export const PdsGoalCalculatorLayout: React.FC< { PdsGoalCalculatorStepEnum.SummaryReport, ]); }); + + const formTypes: DesignationSupportFormType[] = [ + DesignationSupportFormType.Detailed, + DesignationSupportFormType.Simple, + ]; + + const orderedKeysForFormType: Record< + DesignationSupportFormType, + PdsGoalCalculatorStepEnum[] + > = { + [DesignationSupportFormType.Detailed]: [ + PdsGoalCalculatorStepEnum.Setup, + PdsGoalCalculatorStepEnum.ReimbursableExpenses, + PdsGoalCalculatorStepEnum.SupportItem, + PdsGoalCalculatorStepEnum.SummaryReport, + ], + [DesignationSupportFormType.Simple]: [ + PdsGoalCalculatorStepEnum.Setup, + PdsGoalCalculatorStepEnum.SupportItem, + PdsGoalCalculatorStepEnum.SummaryReport, + ], + }; + + it.each(formTypes)( + 'returned step keys match orderedKeysForFormType for %s', + (formType) => { + const { result } = renderHook(() => useSteps(formType)); + expect(result.current.map((step) => step.step)).toEqual( + orderedKeysForFormType[formType], + ); + }, + ); + + it.each(formTypes)( + 'starts with the Setup step for formType %s', + (formType) => { + const { result } = renderHook(() => useSteps(formType)); + expect(result.current[0].step).toBe(PdsGoalCalculatorStepEnum.Setup); + }, + ); + + it.each(formTypes)( + 'ends with the Summary Report step for formType %s', + (formType) => { + const { result } = renderHook(() => useSteps(formType)); + expect(result.current[result.current.length - 1].step).toBe( + PdsGoalCalculatorStepEnum.SummaryReport, + ); + }, + ); + + it.each(formTypes)( + 'returns a non-empty steps array for formType %s', + (formType) => { + const { result } = renderHook(() => useSteps(formType)); + expect(result.current.length).toBeGreaterThan(0); + }, + ); }); diff --git a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx index 18b77b19e8..ee61774231 100644 --- a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx +++ b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx @@ -1,5 +1,6 @@ import { Box, CircularProgress } from '@mui/material'; import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; import { useLocale } from 'src/hooks/useLocale'; import { percentageFormat } from 'src/lib/intlFormat'; @@ -11,6 +12,8 @@ export const CircularProgressWithLabel: React.FC< CircularProgressWithLabelProps > = ({ progress }) => { const locale = useLocale(); + const { t } = useTranslation(); + const formattedProgress = percentageFormat(progress / 100, locale); return ( - {percentageFormat(progress / 100, locale)} + {formattedProgress} diff --git a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx index e76d56731c..56b5774d7f 100644 --- a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx +++ b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx @@ -138,6 +138,7 @@ export const PanelLayout: React.FC = ({ Date: Mon, 11 May 2026 14:27:18 -0400 Subject: [PATCH 06/11] Runs prettier --- .../Shared/PdsGoalCalculatorContext.test.tsx | 9 +++++++-- .../HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index c1e3b83357..f9780734d8 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -247,8 +247,13 @@ describe('PdsGoalCalculatorContext', () => { }> = []; const RecordingProbe: React.FC = () => { - const { currentStep, stepIndex, percentComplete, steps, handleStepChange } = - usePdsGoalCalculator(); + const { + currentStep, + stepIndex, + percentComplete, + steps, + handleStepChange, + } = usePdsGoalCalculator(); renderLog.push({ step: currentStep.step, index: stepIndex, diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts index 44a7473ce6..ac676c84be 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts +++ b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts @@ -18,11 +18,11 @@ export const isSetupComplete = (calculation: Calculation): boolean => { return Boolean( calculation.name && - calculation.status && - calculation.salaryOrHourly && - (calculation.payRate ?? 0) > 0 && - (isSalaried || (calculation.hoursWorkedPerWeek ?? 0) > 0) && - (isPartTime || (calculation.benefits ?? 0) > 0), + calculation.status && + calculation.salaryOrHourly && + (calculation.payRate ?? 0) > 0 && + (isSalaried || (calculation.hoursWorkedPerWeek ?? 0) > 0) && + (isPartTime || (calculation.benefits ?? 0) > 0), ); }; From 156321ea0c78b258f94d9e2f9635da1de745f095 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 14:42:13 -0400 Subject: [PATCH 07/11] Simplify tests --- .../GoalsList/PdsGoalsList.test.tsx | 15 +-- .../Setup/SetupStep.test.tsx | 102 +++++++++++------- .../Autosave/AutosaveTextField.test.tsx | 74 ++++++------- .../Shared/PdsGoalCalculatorContext.test.tsx | 101 ++++------------- .../Shared/useSteps.test.tsx | 58 ---------- .../calculatePdsGoalTotal.test.ts | 17 +-- .../calculations/usePdsSummaryData.test.ts | 22 +--- 7 files changed, 125 insertions(+), 264 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx index f8e0b1cfc7..4f0569e06c 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx @@ -9,12 +9,6 @@ import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper'; import { PdsGoalsList } from './PdsGoalsList'; const mutationSpy = jest.fn(); -const mockEnqueue = jest.fn(); - -jest.mock('notistack', () => ({ - ...jest.requireActual('notistack'), - useSnackbar: () => ({ enqueueSnackbar: mockEnqueue }), -})); type FindByRole = ReturnType['findByRole']; @@ -210,7 +204,7 @@ describe('PdsGoalsList', () => { }); it('shows an error and skips mutation when reimbursement constants are missing for a Default goal', async () => { - const { findByRole } = render( + const { findByRole, findByText } = render( { await openCreateGoalDialog(findByRole); await submitFormType(findByRole, 'Default'); - await waitFor(() => - expect(mockEnqueue).toHaveBeenCalledWith( + expect( + await findByText( 'Could not load required defaults. Please try again or pick Simple.', - { variant: 'error' }, ), - ); + ).toBeInTheDocument(); expect(mutationSpy).not.toHaveGraphqlOperation('CreatePdsGoalCalculation'); await waitFor(() => expect(findByRole('button', { name: 'Create' })).resolves.toBeEnabled(), diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx index 9587ced11f..430ea947f1 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx @@ -104,51 +104,71 @@ describe('SetupStep', () => { ).toBeInTheDocument(); }); - it.each([ - { - name: 'full-time salaried', + it('shows Benefits and hides Hours Worked for full-time salaried', async () => { + const { findByRole, queryByRole } = renderSetup({ calculationMock: fullTimeSalariedMock, - benefits: 'visible' as const, - hoursWorked: 'hidden' as const, - }, - { - name: 'part-time salaried', + }); + + await findByRole('textbox', { name: 'Goal Name' }); + + expect( + await findByRole('spinbutton', { name: 'Benefits' }), + ).toBeInTheDocument(); + await waitFor(() => + expect( + queryByRole('spinbutton', { name: 'Hours Worked' }), + ).not.toBeInTheDocument(), + ); + }); + + it('hides Benefits and Hours Worked for part-time salaried', async () => { + const { findByRole, queryByRole } = renderSetup({ calculationMock: partTimeSalariedMock, - benefits: 'hidden' as const, - hoursWorked: 'hidden' as const, - }, - { - name: 'full-time hourly', + }); + + await findByRole('textbox', { name: 'Goal Name' }); + + await waitFor(() => { + expect( + queryByRole('spinbutton', { name: 'Benefits' }), + ).not.toBeInTheDocument(); + expect( + queryByRole('spinbutton', { name: 'Hours Worked' }), + ).not.toBeInTheDocument(); + }); + }); + + it('shows Benefits and Hours Worked for full-time hourly', async () => { + const { findByRole } = renderSetup({ calculationMock: fullTimeHourlyMock, - benefits: 'visible' as const, - hoursWorked: 'visible' as const, - }, - { - name: 'part-time hourly', + }); + + await findByRole('textbox', { name: 'Goal Name' }); + + expect( + await findByRole('spinbutton', { name: 'Benefits' }), + ).toBeInTheDocument(); + expect( + await findByRole('spinbutton', { name: 'Hours Worked' }), + ).toBeInTheDocument(); + }); + + it('hides Benefits and shows Hours Worked for part-time hourly', async () => { + const { findByRole, queryByRole } = renderSetup({ calculationMock: partTimeHourlyMock, - benefits: 'hidden' as const, - hoursWorked: 'visible' as const, - }, - ])( - 'shows Benefits=$benefits and Hours Worked=$hoursWorked for $name', - async ({ calculationMock, benefits, hoursWorked }) => { - const { findByRole, queryByRole } = renderSetup({ calculationMock }); - - // Wait for the form to load before asserting on conditional fields - await findByRole('textbox', { name: 'Goal Name' }); - - await waitFor(() => - expect({ - benefits: queryByRole('spinbutton', { name: 'Benefits' }) - ? 'visible' - : 'hidden', - hoursWorked: queryByRole('spinbutton', { name: 'Hours Worked' }) - ? 'visible' - : 'hidden', - }).toEqual({ benefits, hoursWorked }), - ); - }, - ); + }); + + await findByRole('textbox', { name: 'Goal Name' }); + + expect( + await findByRole('spinbutton', { name: 'Hours Worked' }), + ).toBeInTheDocument(); + await waitFor(() => + expect( + queryByRole('spinbutton', { name: 'Benefits' }), + ).not.toBeInTheDocument(), + ); + }); it('shows dynamic Pay Rate helper text based on salary type', async () => { const { findByRole, findByText, rerender } = renderSetup({ diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/Autosave/AutosaveTextField.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/Autosave/AutosaveTextField.test.tsx index 8101abfd3b..7e23964a04 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/Autosave/AutosaveTextField.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/Autosave/AutosaveTextField.test.tsx @@ -202,58 +202,48 @@ describe('AutosaveTextField', () => { ); }); - it('hides validation error for an empty untouched field', async () => { + describe('required field', () => { const requiredSchema = yup.object({ name: yup.string().required('Goal Name is required'), }); - const { findByRole } = render( - - - , - ); + const renderRequired = () => + render( + + + , + ); - const input = await findByRole('textbox', { name: 'Goal Name' }); - await waitFor(() => expect(input).toHaveValue('')); + it('hides validation error for an empty untouched field', async () => { + const { findByRole } = renderRequired(); - expect(input).toHaveAccessibleDescription('Enter the goal name'); - expect(input).not.toHaveAttribute('aria-invalid', 'true'); - }); + const input = await findByRole('textbox', { name: 'Goal Name' }); + await waitFor(() => expect(input).toHaveValue('')); - it('shows validation error after the field is touched', async () => { - const requiredSchema = yup.object({ - name: yup.string().required('Goal Name is required'), + expect(input).toHaveAccessibleDescription('Enter the goal name'); + expect(input).not.toHaveAttribute('aria-invalid', 'true'); }); - const { findByRole } = render( - - - , - ); - const input = await findByRole('textbox', { name: 'Goal Name' }); - await waitFor(() => expect(input).toHaveValue('')); + it('shows validation error after the field is touched', async () => { + const { findByRole } = renderRequired(); - fireEvent.focus(input); - fireEvent.blur(input); + const input = await findByRole('textbox', { name: 'Goal Name' }); + await waitFor(() => expect(input).toHaveValue('')); - await waitFor(() => - expect(input).toHaveAccessibleDescription('Goal Name is required'), - ); + fireEvent.focus(input); + fireEvent.blur(input); + + await waitFor(() => + expect(input).toHaveAccessibleDescription('Goal Name is required'), + ); + }); }); describe('select input', () => { diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index f9780734d8..22ddea6ff6 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -9,14 +9,18 @@ import { usePdsGoalCalculator } from './PdsGoalCalculatorContext'; import { useSteps } from './useSteps'; import type { PdsGoalCalculatorStep, PdsGoalCalculatorSteps } from './useSteps'; -jest.mock('./useSteps', () => ({ - __esModule: true, - ...jest.requireActual('./useSteps'), - useSteps: jest.fn(), -})); - -const { useSteps: actualUseSteps } = - jest.requireActual('./useSteps'); +// SWC marks named ESM exports as non-configurable, so jest.spyOn cannot +// redefine `useSteps`. Mock the module with a jest.fn that delegates to the +// real implementation by default — reconcile tests scope their overrides via +// mockReturnValue + an afterEach restore. +jest.mock('./useSteps', () => { + const actual = jest.requireActual('./useSteps'); + return { + __esModule: true, + ...actual, + useSteps: jest.fn(actual.useSteps), + }; +}); const mockedUseSteps = useSteps as jest.MockedFunction; @@ -85,10 +89,6 @@ const StepProbe: React.FC = () => { ); }; -beforeEach(() => { - mockedUseSteps.mockImplementation(actualUseSteps); -}); - describe('PdsGoalCalculatorContext', () => { it('provides steps and current step', async () => { const { result } = renderUsePdsGoalCalculator(); @@ -160,6 +160,12 @@ describe('PdsGoalCalculatorContext', () => { const reconcileMessage = 'Returned to Setup because the current step is no longer available.'; + afterEach(() => { + mockedUseSteps.mockImplementation( + jest.requireActual('./useSteps').useSteps, + ); + }); + it.each([ { name: 'keeps the user on SummaryReport when steps shrink Detailed → Simple', @@ -238,77 +244,6 @@ describe('PdsGoalCalculatorContext', () => { }, ); - it('falls back safely on the one-frame transient render when activeStep is foreign to the new steps array', async () => { - const renderLog: Array<{ - step: PdsGoalCalculatorStepEnum; - index: number; - percent: number; - stepCount: number; - }> = []; - - const RecordingProbe: React.FC = () => { - const { - currentStep, - stepIndex, - percentComplete, - steps, - handleStepChange, - } = usePdsGoalCalculator(); - renderLog.push({ - step: currentStep.step, - index: stepIndex, - percent: percentComplete, - stepCount: steps.length, - }); - return ( - - ); - }; - - mockedUseSteps.mockReturnValue(detailedSteps); - const { getByRole, rerender } = render( - - - , - ); - - userEvent.click(getByRole('button', { name: 'go to reimbursable' })); - - const renderCountBeforeSwitch = renderLog.length; - - mockedUseSteps.mockReturnValue(simpleSteps); - rerender( - - - , - ); - - // The rerender produces a transient render (steps=simple, activeStep still - // ReimbursableExpenses) followed by a reconciled render after the effect - // calls setActiveStep(Setup). Pin both. - const transient = renderLog[renderCountBeforeSwitch]; - expect(transient.stepCount).toBe(3); - // findIndex returns -1 here; the `=== -1 ? 0` fallback at stepIndex memo - // and the `?? steps[0]` fallback on currentStep keep this render safe. - expect(transient.index).toBe(0); - expect(transient.step).toBe(PdsGoalCalculatorStepEnum.Setup); - // Without the -1 fallback, percentComplete would be 0% (Math.round(0/3)); - // pinning 33% guards against a regression that drops the guard. - expect(transient.percent).toBe(33); - - const reconciled = renderLog[renderLog.length - 1]; - expect(reconciled.step).toBe(PdsGoalCalculatorStepEnum.Setup); - expect(reconciled.index).toBe(0); - expect(reconciled.percent).toBe(33); - }); - it('reconciles activeStep state so toggling formType back does not teleport the user', async () => { mockedUseSteps.mockReturnValue(detailedSteps); const { findByTestId, getByRole, rerender } = render( diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx index 2aaa50b05e..72ff2ea48d 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.test.tsx @@ -26,62 +26,4 @@ describe('useSteps', () => { PdsGoalCalculatorStepEnum.SummaryReport, ]); }); - - const formTypes: DesignationSupportFormType[] = [ - DesignationSupportFormType.Detailed, - DesignationSupportFormType.Simple, - ]; - - const orderedKeysForFormType: Record< - DesignationSupportFormType, - PdsGoalCalculatorStepEnum[] - > = { - [DesignationSupportFormType.Detailed]: [ - PdsGoalCalculatorStepEnum.Setup, - PdsGoalCalculatorStepEnum.ReimbursableExpenses, - PdsGoalCalculatorStepEnum.SupportItem, - PdsGoalCalculatorStepEnum.SummaryReport, - ], - [DesignationSupportFormType.Simple]: [ - PdsGoalCalculatorStepEnum.Setup, - PdsGoalCalculatorStepEnum.SupportItem, - PdsGoalCalculatorStepEnum.SummaryReport, - ], - }; - - it.each(formTypes)( - 'returned step keys match orderedKeysForFormType for %s', - (formType) => { - const { result } = renderHook(() => useSteps(formType)); - expect(result.current.map((step) => step.step)).toEqual( - orderedKeysForFormType[formType], - ); - }, - ); - - it.each(formTypes)( - 'starts with the Setup step for formType %s', - (formType) => { - const { result } = renderHook(() => useSteps(formType)); - expect(result.current[0].step).toBe(PdsGoalCalculatorStepEnum.Setup); - }, - ); - - it.each(formTypes)( - 'ends with the Summary Report step for formType %s', - (formType) => { - const { result } = renderHook(() => useSteps(formType)); - expect(result.current[result.current.length - 1].step).toBe( - PdsGoalCalculatorStepEnum.SummaryReport, - ); - }, - ); - - it.each(formTypes)( - 'returns a non-empty steps array for formType %s', - (formType) => { - const { result } = renderHook(() => useSteps(formType)); - expect(result.current.length).toBeGreaterThan(0); - }, - ); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts index 27fd79a857..5fb7cdb324 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts @@ -77,14 +77,15 @@ describe('calculatePdsGoalTotal', () => { }); it('excludes reimbursable expenses and 403b when formType is Simple', () => { - const simple = makeGoal({ formType: DesignationSupportFormType.Simple }); - const result = calculatePdsGoalTotal(simple, defaultConstants); - // With reimbursableTotal=0 and fourOThreeBPercentage=0: - // subtotal = 5400 (salary) + 0 + 0 + 0 + 1500 (benefits) = 6900 - // attrition = 6900 * 0.06 = 414 - // creditCardFees = (6900 + 414) * 0.06 = 438.84 - // assessment = (6900 + 438.84 + 414) * 0.12 ≈ 930.34 - expect(result).toBeCloseTo(930.34, 1); + const simple = calculatePdsGoalTotal( + makeGoal({ formType: DesignationSupportFormType.Simple }), + { ...defaultConstants, fourOThreeBPercentage: 0.1 }, + ); + const detailed = calculatePdsGoalTotal( + makeGoal({ formType: DesignationSupportFormType.Detailed }), + { ...defaultConstants, fourOThreeBPercentage: 0.1 }, + ); + expect(simple).toBeLessThan(detailed); }); it('treats null formType the same as Detailed (legacy goals)', () => { diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index fc8a70e068..9e58409a7c 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -340,7 +340,7 @@ describe('usePdsSummaryData', () => { }); describe('Simple form type', () => { - it('zeroes reimbursableTotal in otherConstants when formType is Simple', () => { + it('zeroes reimbursable + 403b but preserves reimbursableTotals', () => { const calc = { ...defaultCalculation, formType: DesignationSupportFormType.Simple, @@ -349,27 +349,7 @@ describe('usePdsSummaryData', () => { usePdsSummaryData(calc, defaultHcmUser), ); expect(result.current?.otherConstants.reimbursableTotal).toBe(0); - }); - - it('zeroes fourOThreeBPercentage in otherConstants when formType is Simple', () => { - const calc = { - ...defaultCalculation, - formType: DesignationSupportFormType.Simple, - }; - const { result } = renderHook(() => - usePdsSummaryData(calc, defaultHcmUser), - ); expect(result.current?.otherConstants.fourOThreeBPercentage).toBe(0); - }); - - it('still computes reimbursableTotals (saved values are preserved)', () => { - const calc = { - ...defaultCalculation, - formType: DesignationSupportFormType.Simple, - }; - const { result } = renderHook(() => - usePdsSummaryData(calc, defaultHcmUser), - ); // The reimbursableTotals object is still computed (data isn't lost), // but the otherConstants.reimbursableTotal that feeds the Other Expenses // calculation is zeroed. From 6e107490490dc8a6073874dd8eb3fd411d96b69d Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 14:54:59 -0400 Subject: [PATCH 08/11] PR fixes --- .../SharedComponents/SectionList.test.tsx | 11 +++ .../ReimbursableExpensesSectionList.test.tsx | 80 +++++++++++++++++++ .../Setup/SetupSectionList.test.tsx | 35 ++++++++ .../Shared/PdsGoalCalculatorContext.test.tsx | 20 +++++ .../Shared/PdsGoalCalculatorLayout.test.tsx | 20 ++++- .../Shared/PdsGoalCalculatorLayout.tsx | 2 +- .../SummaryReportSectionList.test.tsx | 19 +++++ .../SupportItemSectionList.test.tsx | 43 ++++++++++ .../CircularProgressWithLabel.test.tsx | 54 +++++++++++++ .../CircularProgressWithLabel.tsx | 56 ++++++++----- .../PanelLayout/PanelLayout.test.tsx | 25 ++++++ .../PanelLayout/PanelLayout.tsx | 7 +- 12 files changed, 347 insertions(+), 25 deletions(-) create mode 100644 src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.test.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.test.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/SummaryReport/SummaryReportSectionList.test.tsx create mode 100644 src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.test.tsx create mode 100644 src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx diff --git a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx index b8b446449b..184edca021 100644 --- a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx +++ b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx @@ -31,6 +31,17 @@ describe('SectionList', () => { within(incompleteSection).getByTestId('RadioButtonUncheckedIcon'), ).toBeInTheDocument(); }); + + it('announces Complete/Incomplete to assistive tech', () => { + const { getByTitle } = render( + + + , + ); + + expect(getByTitle('Complete')).toBeInTheDocument(); + expect(getByTitle('Incomplete')).toBeInTheDocument(); + }); }); describe('ReportSectionList', () => { diff --git a/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.test.tsx b/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.test.tsx new file mode 100644 index 0000000000..d0b44e2ffa --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/ReimbursableExpenses/ReimbursableExpensesSectionList.test.tsx @@ -0,0 +1,80 @@ +import { render, waitFor, within } from '@testing-library/react'; +import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper'; +import { ReimbursableExpensesSectionList } from './ReimbursableExpensesSectionList'; + +const allReimbursableUntouched = { + ministryCellPhone: null, + ministryInternet: null, + mpdNewsletter: null, + mpdMiscellaneous: null, + accountTransfers: null, + otherMonthlyReimbursements: null, + conferenceRetreatCosts: null, + ministryTravelMeals: null, + otherAnnualReimbursements: null, +}; + +describe('ReimbursableExpensesSectionList', () => { + it('renders both sections as incomplete when no reimbursable fields have been touched', async () => { + const { findAllByRole } = render( + + + , + ); + + const items = await findAllByRole('listitem'); + expect(items).toHaveLength(2); + + const [monthly, annual] = items; + expect(monthly).toHaveTextContent('Monthly Reimbursable Expenses'); + expect(annual).toHaveTextContent('Annual Reimbursable Expenses'); + + await waitFor(() => { + expect( + within(monthly).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + expect( + within(annual).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + }); + }); + + it('marks only Monthly complete when a monthly field is touched', async () => { + const { findAllByRole } = render( + + + , + ); + + const [monthly, annual] = await findAllByRole('listitem'); + await waitFor(() => { + expect(within(monthly).getByTestId('CircleIcon')).toBeInTheDocument(); + expect( + within(annual).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + }); + }); + + it('marks only Annual complete when an annual field is touched', async () => { + const { findAllByRole } = render( + + + , + ); + + const [monthly, annual] = await findAllByRole('listitem'); + await waitFor(() => { + expect( + within(monthly).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + expect(within(annual).getByTestId('CircleIcon')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.test.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.test.tsx new file mode 100644 index 0000000000..7db17a0ee2 --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupSectionList.test.tsx @@ -0,0 +1,35 @@ +import { render, waitFor, within } from '@testing-library/react'; +import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper'; +import { SetupSectionList } from './SetupSectionList'; + +describe('SetupSectionList', () => { + it('renders the Setup section as complete when calculation has all required fields', async () => { + const { findByRole } = render( + + + , + ); + + const setupItem = await findByRole('listitem'); + expect(setupItem).toHaveTextContent('Setup'); + await waitFor(() => + expect(within(setupItem).getByTestId('CircleIcon')).toBeInTheDocument(), + ); + }); + + it('renders the Setup section as incomplete when calculation is missing required fields', async () => { + const { findByRole } = render( + + + , + ); + + const setupItem = await findByRole('listitem'); + expect(setupItem).toHaveTextContent('Setup'); + await waitFor(() => + expect( + within(setupItem).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(), + ); + }); +}); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index 22ddea6ff6..3354fff92a 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -156,6 +156,26 @@ describe('PdsGoalCalculatorContext', () => { expect(result.current.percentComplete).toBe(100); }); + it('rounds percentComplete to 33/67/100 for the 3-step Simple form', async () => { + const { result } = renderHook(() => usePdsGoalCalculator(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => expect(result.current.steps).toHaveLength(3)); + + expect(result.current.percentComplete).toBe(33); + act(() => result.current.handleContinue()); + expect(result.current.percentComplete).toBe(67); + act(() => result.current.handleContinue()); + expect(result.current.percentComplete).toBe(100); + }); + describe('preserves the user step when the steps array changes', () => { const reconcileMessage = 'Returned to Setup because the current step is no longer available.'; diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx index 60e643f126..38e803073a 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx @@ -29,7 +29,25 @@ describe('PdsGoalCalculatorLayout', () => { , ); - const progressIndicator = await findByRole('progressbar'); + const progressIndicator = await findByRole('progressbar', { + name: 'Section progress', + }); expect(progressIndicator).toHaveAttribute('aria-valuenow', '25'); }); + + it('shows an indeterminate progress indicator while calculation is loading', async () => { + const { findByRole } = render( + + Section List} + mainContent={
Main Content
} + /> +
, + ); + + const loadingIndicator = await findByRole('progressbar', { + name: 'Calculating progress', + }); + expect(loadingIndicator).not.toHaveAttribute('aria-valuenow'); + }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx index e2034a72b9..517a7e7576 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx @@ -48,7 +48,7 @@ export const PdsGoalCalculatorLayout: React.FC< { + it('renders the MPD Goal section as complete when summary data has a positive overall total', async () => { + const { findByRole } = render( + + + , + ); + + const mpdGoalItem = await findByRole('listitem'); + expect(mpdGoalItem).toHaveTextContent('MPD Goal'); + await waitFor(() => + expect(within(mpdGoalItem).getByTestId('CircleIcon')).toBeInTheDocument(), + ); + }); +}); diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.test.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.test.tsx new file mode 100644 index 0000000000..731513e3c0 --- /dev/null +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.test.tsx @@ -0,0 +1,43 @@ +import { render, waitFor, within } from '@testing-library/react'; +import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper'; +import { SupportItemSectionList } from './SupportItemSectionList'; + +describe('SupportItemSectionList', () => { + it('renders Salary and Other as complete when setup is complete', async () => { + const { findAllByRole } = render( + + + , + ); + + const items = await findAllByRole('listitem'); + expect(items).toHaveLength(2); + + const [salary, other] = items; + expect(salary).toHaveTextContent('Salary'); + expect(other).toHaveTextContent('Other'); + + await waitFor(() => { + expect(within(salary).getByTestId('CircleIcon')).toBeInTheDocument(); + expect(within(other).getByTestId('CircleIcon')).toBeInTheDocument(); + }); + }); + + it('renders Salary and Other as incomplete when setup is incomplete', async () => { + const { findAllByRole } = render( + + + , + ); + + const [salary, other] = await findAllByRole('listitem'); + await waitFor(() => { + expect( + within(salary).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + expect( + within(other).getByTestId('RadioButtonUncheckedIcon'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx new file mode 100644 index 0000000000..b8a2bd6eef --- /dev/null +++ b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { render } from '__tests__/util/testingLibraryReactMock'; +import theme from 'src/theme'; +import { CircularProgressWithLabel } from './CircularProgressWithLabel'; + +interface TestComponentProps { + progress?: number; + loading?: boolean; +} + +const TestComponent: React.FC = ({ + progress = 0, + loading, +}) => ( + + + +); + +describe('CircularProgressWithLabel', () => { + it('renders the determinate progressbar with a static accessible name and aria-valuenow', () => { + const { getByRole } = render(); + + const progressbar = getByRole('progressbar', { name: 'Section progress' }); + expect(progressbar).toHaveAttribute('aria-valuenow', '25'); + }); + + it('does not duplicate the percentage in the accessible name', () => { + const { getByRole } = render(); + + expect( + getByRole('progressbar', { name: 'Section progress' }), + ).toBeInTheDocument(); + }); + + it('hides the visual percentage label from assistive tech', () => { + const { getByText } = render(); + + expect(getByText('25%')).toHaveAttribute('aria-hidden', 'true'); + }); + + it('renders an indeterminate progressbar with a calculating label when loading', () => { + const { getByRole, queryByText } = render( + , + ); + + const progressbar = getByRole('progressbar', { + name: 'Calculating progress', + }); + expect(progressbar).not.toHaveAttribute('aria-valuenow'); + expect(queryByText('25%')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx index ee61774231..e40915f3d1 100644 --- a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx +++ b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx @@ -6,11 +6,12 @@ import { percentageFormat } from 'src/lib/intlFormat'; interface CircularProgressWithLabelProps { progress: number; + loading?: boolean; } export const CircularProgressWithLabel: React.FC< CircularProgressWithLabelProps -> = ({ progress }) => { +> = ({ progress, loading = false }) => { const locale = useLocale(); const { t } = useTranslation(); const formattedProgress = percentageFormat(progress / 100, locale); @@ -22,27 +23,38 @@ export const CircularProgressWithLabel: React.FC< alignItems: 'center', }} > - - - - {formattedProgress} - - + {loading ? ( + + ) : ( + <> + + + + {formattedProgress} + + + + )} ); }; diff --git a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx index cefe768a69..0e116d9bd8 100644 --- a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx +++ b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx @@ -170,6 +170,8 @@ describe('PanelLayout', () => { expect(inactiveButton).toHaveStyle({ color: theme.palette.mpdxGrayDark.main, }); + expect(activeButton).toHaveAttribute('aria-current', 'step'); + expect(inactiveButton).not.toHaveAttribute('aria-current'); }); it('scrolls main content to top when currentIndex changes', async () => { @@ -187,6 +189,29 @@ describe('PanelLayout', () => { await waitFor(() => expect(mainContent.scrollTop).toBe(0)); }); + it('renders an indeterminate progress indicator when progressLoading is true', () => { + const { getByRole, queryByText } = render( + , + ); + + const progressIndicator = getByRole('progressbar'); + expect(progressIndicator).toBeInTheDocument(); + expect(progressIndicator).not.toHaveAttribute('aria-valuenow'); + expect(queryByText('42%')).not.toBeInTheDocument(); + }); + + it('hides the progress indicator when showPercentage is false', () => { + const { queryByRole } = render( + , + ); + + expect(queryByRole('progressbar')).not.toBeInTheDocument(); + }); + it('handles empty icon panel items gracefully', () => { const { getByTestId, queryAllByRole } = render( = ({ panelType, percentComplete, showPercentage = true, + progressLoading = false, icons, sidebarContent, backHref, @@ -130,7 +132,10 @@ export const PanelLayout: React.FC = ({ <> {showPercentage && ( - + )} From 7239959dfd0429e52b9e5a395a889e92518595f3 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Mon, 11 May 2026 15:20:07 -0400 Subject: [PATCH 09/11] Refactor --- .../GoalsList/CreateGoalDialog.tsx | 47 ++++++++++--------- .../PdsGoalCalculator/Setup/SetupStep.tsx | 15 +++--- .../Shared/PdsGoalCalculatorContext.tsx | 10 ++-- .../PdsGoalCalculator/Shared/formType.test.ts | 12 ----- .../PdsGoalCalculator/Shared/formType.ts | 5 -- .../Shared/pdsCompletion.test.ts | 11 ----- .../PdsGoalCalculator/Shared/pdsCompletion.ts | 5 -- .../PdsGoalCalculator/Shared/useSteps.tsx | 3 +- .../SummaryReport/PdsSummaryTable.tsx | 5 +- .../SupportItem/SupportItemSectionList.tsx | 10 ++-- .../SupportItem/otherBreakdown.tsx | 5 +- .../calculations/calculatePdsGoalTotal.ts | 3 +- 12 files changed, 48 insertions(+), 83 deletions(-) delete mode 100644 src/components/HrTools/PdsGoalCalculator/Shared/formType.test.ts delete mode 100644 src/components/HrTools/PdsGoalCalculator/Shared/formType.ts diff --git a/src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx b/src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx index bb967461a0..066142e62c 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Button, CircularProgress, @@ -50,26 +50,31 @@ export const CreateGoalDialog: React.FC = ({ } }, [open]); - const formTypeOptions: Array<{ - value: DesignationSupportFormType; - title: string; - description: string; - }> = [ - { - value: DesignationSupportFormType.Detailed, - title: t('Default'), - description: t( - 'Full calculator with reimbursable expenses and 403b contributions.', - ), - }, - { - value: DesignationSupportFormType.Simple, - title: t('Simple'), - description: t( - 'Streamlined calculator without reimbursable expenses or 403b contributions.', - ), - }, - ]; + const formTypeOptions = useMemo< + Array<{ + value: DesignationSupportFormType; + title: string; + description: string; + }> + >( + () => [ + { + value: DesignationSupportFormType.Detailed, + title: t('Default'), + description: t( + 'Full calculator with reimbursable expenses and 403b contributions.', + ), + }, + { + value: DesignationSupportFormType.Simple, + title: t('Simple'), + description: t( + 'Streamlined calculator without reimbursable expenses or 403b contributions.', + ), + }, + ], + [t], + ); return ( { const isSimpleForm = calculation?.formType === DesignationSupportFormType.Simple; + const fourOThreeB = hcmUser?.fourOThreeB; + const totalFourOThreeBContributionPercentage = fourOThreeB + ? (fourOThreeB.currentTaxDeferredContributionPercentage ?? 0) + + (fourOThreeB.currentRothContributionPercentage ?? 0) + : ''; + const payRateHelperText = isSalaried ? t('Enter yearly salary') : t('Enter hourly rate'); @@ -253,14 +259,7 @@ export const SetupStep: React.FC = () => { variant="outlined" label={t('403b Contribution Percentage')} disabled - value={ - hcmUser?.fourOThreeB - ? (hcmUser.fourOThreeB - .currentTaxDeferredContributionPercentage ?? 0) + - (hcmUser.fourOThreeB.currentRothContributionPercentage ?? - 0) - : '' - } + value={totalFourOThreeBContributionPercentage} helperText={t( 'Retrieved from Principal. A combined percentage of your current tax deferred and Roth contributions.', )} diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index 640ff8c212..c96bb9025d 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -10,6 +10,7 @@ import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { DesignationSupportFormType } from 'src/graphql/types.generated'; import { useTrackMutation } from 'src/hooks/useTrackMutation'; +import { safeProgressRatio } from '../../GoalCalculator/Shared/safeProgressRatio'; import { PdsGoalCalculationFieldsFragment, usePdsGoalCalculationQuery, @@ -123,13 +124,10 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { return idx === -1 ? 0 : idx; }, [steps, activeStep]); - // steps is a non-empty tuple, so steps[0] is guaranteed defined; the fallback - // protects against an out-of-range stepIndex. - const currentStep = steps[stepIndex] ?? steps[0]; + const currentStep = steps[stepIndex]; - const percentComplete = useMemo( - () => Math.round(((stepIndex + 1) / steps.length) * 100), - [stepIndex, steps.length], + const percentComplete = Math.round( + safeProgressRatio(stepIndex + 1, steps.length) * 100, ); const handleStepChange = useCallback( diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/formType.test.ts b/src/components/HrTools/PdsGoalCalculator/Shared/formType.test.ts deleted file mode 100644 index 584e480267..0000000000 --- a/src/components/HrTools/PdsGoalCalculator/Shared/formType.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DesignationSupportFormType } from 'src/graphql/types.generated'; -import { isSimpleFormType } from './formType'; - -describe('isSimpleFormType', () => { - it('returns true for Simple form type', () => { - expect(isSimpleFormType(DesignationSupportFormType.Simple)).toBe(true); - }); - - it('returns false for Detailed form type', () => { - expect(isSimpleFormType(DesignationSupportFormType.Detailed)).toBe(false); - }); -}); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/formType.ts b/src/components/HrTools/PdsGoalCalculator/Shared/formType.ts deleted file mode 100644 index 1b21671a75..0000000000 --- a/src/components/HrTools/PdsGoalCalculator/Shared/formType.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { DesignationSupportFormType } from 'src/graphql/types.generated'; - -export const isSimpleFormType = ( - formType: DesignationSupportFormType, -): boolean => formType === DesignationSupportFormType.Simple; diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts index f4affa1307..eb9269de4a 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.test.ts @@ -8,8 +8,6 @@ import { isAnnualReimbursableComplete, isMonthlyReimbursableComplete, isMpdGoalComplete, - isOtherComplete, - isSalaryComplete, isSetupComplete, } from './pdsCompletion'; @@ -115,15 +113,6 @@ describe('isAnnualReimbursableComplete', () => { }); }); -describe('isSalaryComplete / isOtherComplete', () => { - it('mirror isSetupComplete', () => { - expect(isSalaryComplete(baseCalculation)).toBe(true); - expect(isOtherComplete(baseCalculation)).toBe(true); - expect(isSalaryComplete({ ...baseCalculation, payRate: null })).toBe(false); - expect(isOtherComplete({ ...baseCalculation, payRate: null })).toBe(false); - }); -}); - describe('isMpdGoalComplete', () => { it('returns false when summaryData is null', () => { expect(isMpdGoalComplete(null)).toBe(false); diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts index ac676c84be..26fa90ff71 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts +++ b/src/components/HrTools/PdsGoalCalculator/Shared/pdsCompletion.ts @@ -61,11 +61,6 @@ export const isAnnualReimbursableComplete = ( ]); }; -// Salary and Other are read-only derived tables whose only required inputs -// come from the Setup step. -export const isSalaryComplete = isSetupComplete; -export const isOtherComplete = isSetupComplete; - export const isMpdGoalComplete = ( summaryData: PdsSummaryData | null, ): boolean => !!summaryData && summaryData.overallTotal > 0; diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx index c87380e0d4..6fb5df552d 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/useSteps.tsx @@ -6,7 +6,6 @@ import SettingsIcon from '@mui/icons-material/Settings'; import { useTranslation } from 'react-i18next'; import { DesignationSupportFormType } from 'src/graphql/types.generated'; import { PdsGoalCalculatorStepEnum } from '../PdsGoalCalculatorHelper'; -import { isSimpleFormType } from './formType'; export interface PdsGoalCalculatorStep { step: PdsGoalCalculatorStepEnum; @@ -46,7 +45,7 @@ export const useSteps = ( icon: , }; - return isSimpleFormType(formType) + return formType === DesignationSupportFormType.Simple ? [setup, supportItem, summaryReport] : [setup, reimbursableExpenses, supportItem, summaryReport]; }, [t, formType]); diff --git a/src/components/HrTools/PdsGoalCalculator/SummaryReport/PdsSummaryTable.tsx b/src/components/HrTools/PdsGoalCalculator/SummaryReport/PdsSummaryTable.tsx index 478bf40256..47a17ad84d 100644 --- a/src/components/HrTools/PdsGoalCalculator/SummaryReport/PdsSummaryTable.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SummaryReport/PdsSummaryTable.tsx @@ -15,7 +15,6 @@ import { } from 'src/lib/intlFormat'; import { safeProgressRatio } from '../../GoalCalculator/Shared/safeProgressRatio'; import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; -import { isSimpleFormType } from '../Shared/formType'; import { PdsSummaryHeaderCards } from './PdsSummaryHeaderCards'; interface PdsSummaryRow { @@ -89,9 +88,7 @@ export const PdsSummaryTable: React.FC = ({ const isFullTime = calculation.status === DesignationSupportStatus.FullTime; const isPartTime = calculation.status === DesignationSupportStatus.PartTime; - const isSimple = isSimpleFormType( - calculation.formType ?? DesignationSupportFormType.Detailed, - ); + const isSimple = calculation.formType === DesignationSupportFormType.Simple; const rows: PdsSummaryRow[] = [ // Salary section diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx index 7c88a86e0f..c9ed4fd365 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/SupportItemSectionList.tsx @@ -2,17 +2,21 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { SectionList } from '../../GoalCalculator/SharedComponents/SectionList'; import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext'; -import { isOtherComplete, isSalaryComplete } from '../Shared/pdsCompletion'; +import { isSetupComplete } from '../Shared/pdsCompletion'; export const SupportItemSectionList: React.FC = () => { const { t } = useTranslation(); const { calculation } = usePdsGoalCalculator(); + // Salary and Other are read-only derived tables whose only required inputs + // come from the Setup step. + const isComplete = isSetupComplete(calculation); + return ( ); diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx index e5f381063d..7937838ce1 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx @@ -8,7 +8,6 @@ import { DesignationSupportStatus, } from 'src/graphql/types.generated'; import { currencyFormat, percentageFormat } from 'src/lib/intlFormat'; -import { isSimpleFormType } from '../Shared/formType'; import { OtherExpensesConstants, OtherExpensesFields, @@ -44,9 +43,7 @@ export const buildOtherBreakdownRows = ( constants, ); - const isSimple = isSimpleFormType( - calculation.formType ?? DesignationSupportFormType.Detailed, - ); + const isSimple = calculation.formType === DesignationSupportFormType.Simple; const rows: OtherBreakdownRow[] = [ ...(isSimple diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts index 4a7b8c7e6f..36e9aad328 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts @@ -4,7 +4,6 @@ import { GoalMiscConstants, } from 'src/hooks/useGoalCalculatorConstants'; import { HcmUserQuery } from '../Shared/HCM.generated'; -import { isSimpleFormType } from '../Shared/formType'; import { OtherExpensesConstants, OtherExpensesFields, @@ -85,7 +84,7 @@ export const buildOtherExpensesConstants = ( salaryTotals: SalaryTotals, reimbursableTotal: number, ): OtherExpensesConstants => { - const isSimple = isSimpleFormType(formType); + const isSimple = formType === DesignationSupportFormType.Simple; return { reimbursableTotal: isSimple ? 0 : reimbursableTotal, salarySubtotal: salaryTotals.subtotal, From 65607ccfcddc9bfbb273406b41c113bbbaa6b8cb Mon Sep 17 00:00:00 2001 From: wjames111 Date: Tue, 12 May 2026 16:31:23 -0400 Subject: [PATCH 10/11] PR fixes --- .../SharedComponents/SectionList.test.tsx | 2 +- .../Shared/PdsGoalCalculatorContext.test.tsx | 11 +---------- .../Shared/PdsGoalCalculatorLayout.test.tsx | 2 +- .../calculatePdsGoalTotal.test.ts | 19 +++++++++++++++---- .../CircularProgressWithLabel.test.tsx | 4 ++-- .../CircularProgressWithLabel.tsx | 2 +- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx index 184edca021..f078d2dd20 100644 --- a/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx +++ b/src/components/HrTools/GoalCalculator/SharedComponents/SectionList.test.tsx @@ -32,7 +32,7 @@ describe('SectionList', () => { ).toBeInTheDocument(); }); - it('announces Complete/Incomplete to assistive tech', () => { + it('renders the Complete/Incomplete title', () => { const { getByTitle } = render( diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx index 3354fff92a..50ba006bd7 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.test.tsx @@ -9,10 +9,7 @@ import { usePdsGoalCalculator } from './PdsGoalCalculatorContext'; import { useSteps } from './useSteps'; import type { PdsGoalCalculatorStep, PdsGoalCalculatorSteps } from './useSteps'; -// SWC marks named ESM exports as non-configurable, so jest.spyOn cannot -// redefine `useSteps`. Mock the module with a jest.fn that delegates to the -// real implementation by default — reconcile tests scope their overrides via -// mockReturnValue + an afterEach restore. +// `jest.spyOn` can't redefine, so we mock the module with a `jest.fn` that delegates to the real implementation, giving reconcile tests a handle to override the return value. jest.mock('./useSteps', () => { const actual = jest.requireActual('./useSteps'); return { @@ -180,12 +177,6 @@ describe('PdsGoalCalculatorContext', () => { const reconcileMessage = 'Returned to Setup because the current step is no longer available.'; - afterEach(() => { - mockedUseSteps.mockImplementation( - jest.requireActual('./useSteps').useSteps, - ); - }); - it.each([ { name: 'keeps the user on SummaryReport when steps shrink Detailed → Simple', diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx index 38e803073a..437e613c14 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.test.tsx @@ -30,7 +30,7 @@ describe('PdsGoalCalculatorLayout', () => { ); const progressIndicator = await findByRole('progressbar', { - name: 'Section progress', + name: 'Form Progress', }); expect(progressIndicator).toHaveAttribute('aria-valuenow', '25'); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts index 5fb7cdb324..07b53fd5ee 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts @@ -77,15 +77,26 @@ describe('calculatePdsGoalTotal', () => { }); it('excludes reimbursable expenses and 403b when formType is Simple', () => { - const simple = calculatePdsGoalTotal( + const baseline = calculatePdsGoalTotal( makeGoal({ formType: DesignationSupportFormType.Simple }), { ...defaultConstants, fourOThreeBPercentage: 0.1 }, ); - const detailed = calculatePdsGoalTotal( - makeGoal({ formType: DesignationSupportFormType.Detailed }), + + const withDifferentReimbursables = calculatePdsGoalTotal( + makeGoal({ + formType: DesignationSupportFormType.Simple, + ministryCellPhone: 9999, + otherAnnualReimbursements: 9999, + }), { ...defaultConstants, fourOThreeBPercentage: 0.1 }, ); - expect(simple).toBeLessThan(detailed); + expect(withDifferentReimbursables).toBeCloseTo(baseline); + + const withDifferent403b = calculatePdsGoalTotal( + makeGoal({ formType: DesignationSupportFormType.Simple }), + { ...defaultConstants, fourOThreeBPercentage: 0.5 }, + ); + expect(withDifferent403b).toBeCloseTo(baseline); }); it('treats null formType the same as Detailed (legacy goals)', () => { diff --git a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx index b8a2bd6eef..e001f9cc7a 100644 --- a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx +++ b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.test.tsx @@ -22,7 +22,7 @@ describe('CircularProgressWithLabel', () => { it('renders the determinate progressbar with a static accessible name and aria-valuenow', () => { const { getByRole } = render(); - const progressbar = getByRole('progressbar', { name: 'Section progress' }); + const progressbar = getByRole('progressbar', { name: 'Form Progress' }); expect(progressbar).toHaveAttribute('aria-valuenow', '25'); }); @@ -30,7 +30,7 @@ describe('CircularProgressWithLabel', () => { const { getByRole } = render(); expect( - getByRole('progressbar', { name: 'Section progress' }), + getByRole('progressbar', { name: 'Form Progress' }), ).toBeInTheDocument(); }); diff --git a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx index e40915f3d1..26917dd12a 100644 --- a/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx +++ b/src/components/HrTools/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel.tsx @@ -37,7 +37,7 @@ export const CircularProgressWithLabel: React.FC< value={progress} size={28} thickness={4} - aria-label={t('Section progress')} + aria-label={t('Form Progress')} /> Date: Wed, 13 May 2026 09:33:09 -0400 Subject: [PATCH 11/11] Add test for 403b --- .../calculatePdsGoalTotal.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts index 07b53fd5ee..d5c613222a 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts @@ -99,6 +99,29 @@ describe('calculatePdsGoalTotal', () => { expect(withDifferent403b).toBeCloseTo(baseline); }); + it('includes reimbursable expenses and 403b when formType is Detailed', () => { + const baseline = calculatePdsGoalTotal( + makeGoal({ formType: DesignationSupportFormType.Detailed }), + { ...defaultConstants, fourOThreeBPercentage: 0.1 }, + ); + + const withDifferentReimbursables = calculatePdsGoalTotal( + makeGoal({ + formType: DesignationSupportFormType.Detailed, + ministryCellPhone: 9999, + otherAnnualReimbursements: 9999, + }), + { ...defaultConstants, fourOThreeBPercentage: 0.1 }, + ); + expect(withDifferentReimbursables).toBeGreaterThan(baseline); + + const withDifferent403b = calculatePdsGoalTotal( + makeGoal({ formType: DesignationSupportFormType.Detailed }), + { ...defaultConstants, fourOThreeBPercentage: 0.5 }, + ); + expect(withDifferent403b).toBeGreaterThan(baseline); + }); + it('treats null formType the same as Detailed (legacy goals)', () => { const legacy = makeGoal({ formType: null }); const detailed = makeGoal({