From 009efb1863d63c82f27c95b14a7f45e1427fec3c Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Tue, 19 May 2026 09:09:03 -0500 Subject: [PATCH 1/8] Block sidepanel icons on Settings page --- .../PdsGoalCalculator/Setup/SetupStep.tsx | 18 ++++++++-- .../Shared/PdsGoalCalculatorContext.tsx | 10 ++++++ .../Shared/PdsGoalCalculatorLayout.tsx | 28 ++++++++++----- .../PanelLayout/PanelLayout.tsx | 34 +++++++++++-------- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx index 83d60fd6fd..837e360975 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import CalculateIcon from '@mui/icons-material/Calculate'; import { Autocomplete, @@ -17,6 +17,7 @@ import { import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; +import { useAutosaveForm } from 'src/components/Shared/Autosave/AutosaveForm'; import { useGetUserQuery } from 'src/components/User/GetUser.generated'; import { DesignationSupportFormType, @@ -38,8 +39,19 @@ import { HoursPerWeekGrid } from './HoursPerWeekGrid/HoursPerWeekGrid'; export const SetupStep: React.FC = () => { const { t } = useTranslation(); const theme = useTheme(); - const { calculation, hcmUser, isMutating, setRightPanelContent } = - usePdsGoalCalculator(); + const { + calculation, + hcmUser, + isMutating, + setRightPanelContent, + setSetupStepValid, + } = usePdsGoalCalculator(); + const { allValid } = useAutosaveForm(); + + useEffect(() => { + setSetupStepValid(allValid); + return () => setSetupStepValid(false); + }, [allValid, setSetupStepValid]); const { data: userData } = useGetUserQuery(); const schema = useMemo( () => diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index f2442c6947..99baddec10 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -48,6 +48,11 @@ export type PdsGoalCalculatorType = { stepIndex: number; isDrawerOpen: boolean; + + /** Whether all required Setup step fields are valid */ + isSetupStepValid: boolean; + setSetupStepValid: (valid: boolean) => void; + handleStepChange: (stepId: PdsGoalCalculatorStepEnum) => void; handleContinue: () => void; handlePreviousStep: () => void; @@ -103,6 +108,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const steps = useSteps( calculation?.formType ?? DesignationSupportFormType.Detailed, ); + const [isSetupStepValid, setSetupStepValid] = useState(false); const [rightPanelContent, setRightPanelContent] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(true); @@ -177,6 +183,8 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { hcmUser, rightPanelContent, isDrawerOpen, + isSetupStepValid, + setSetupStepValid, handleStepChange, handleContinue, handlePreviousStep, @@ -198,6 +206,8 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { hcmUser, rightPanelContent, isDrawerOpen, + isSetupStepValid, + setSetupStepValid, handleStepChange, handleContinue, handlePreviousStep, diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx index 517a7e7576..5e7bdf07f9 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx @@ -19,6 +19,7 @@ export const PdsGoalCalculatorLayout: React.FC< const { steps, currentStep, + isSetupStepValid, handleStepChange, isDrawerOpen, setDrawerOpen, @@ -27,22 +28,33 @@ export const PdsGoalCalculatorLayout: React.FC< calculationLoading, } = usePdsGoalCalculator(); + const navigationDisabled = + currentStep.step === PdsGoalCalculatorStepEnum.Setup && !isSetupStepValid; + const handleStepIconClick = (step: PdsGoalCalculatorStepEnum) => { if (currentStep.step === step) { toggleDrawer(); - } else { + } else if (!navigationDisabled) { handleStepChange(step); setDrawerOpen(true); } }; - const iconPanelItems = steps.map((step) => ({ - key: step.step, - icon: step.icon, - label: step.title, - isActive: currentStep.step === step.step, - onClick: () => handleStepIconClick(step.step), - })); + const iconPanelItems = steps.map((step) => { + const isOtherStep = currentStep.step !== step.step; + const isDisabled = navigationDisabled && isOtherStep; + return { + key: step.step, + icon: step.icon, + label: step.title, + isActive: currentStep.step === step.step, + disabled: isDisabled, + tooltip: isDisabled + ? t('Complete all required fields to continue') + : undefined, + onClick: () => handleStepIconClick(step.step), + }; + }); return ( void; } @@ -140,19 +142,23 @@ export const PanelLayout: React.FC = ({ )} {icons?.map((item) => ( - - {item.icon} - + + + + {item.icon} + + + ))} {(backHref || onBack) && ( Date: Tue, 19 May 2026 09:20:45 -0500 Subject: [PATCH 2/8] Add tests for icon button behavior in PanelLayout component --- .../PdsGoalCalculator.test.tsx | 56 +++++++++++++++++++ .../PanelLayout/PanelLayout.test.tsx | 53 +++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx index 6335996a6d..8aa20521e2 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -158,6 +158,62 @@ describe('PdsGoalCalculator', () => { ).toBeInTheDocument(); }); + it('disables non-Setup step icons when Setup fields are invalid', async () => { + const { findByRole } = render( + + + , + ); + + await waitFor(async () => { + expect( + await findByRole('button', { name: 'Reimbursable Expenses' }), + ).toBeDisabled(); + }); + expect(await findByRole('button', { name: 'Support Item' })).toBeDisabled(); + expect( + await findByRole('button', { name: 'Summary Report' }), + ).toBeDisabled(); + }); + + it('enables non-Setup step icons when all Setup fields are valid', async () => { + const { findByRole } = render( + + + , + ); + + await waitFor(async () => { + expect( + await findByRole('button', { name: 'Reimbursable Expenses' }), + ).not.toBeDisabled(); + }); + expect( + await findByRole('button', { name: 'Support Item' }), + ).not.toBeDisabled(); + expect( + await findByRole('button', { name: 'Summary Report' }), + ).not.toBeDisabled(); + }); + + it('shows tooltip on disabled side panel icons', async () => { + const { findByRole, findByText } = render( + + + , + ); + + const reimbursableButton = await findByRole('button', { + name: 'Reimbursable Expenses', + }); + await waitFor(() => expect(reimbursableButton).toBeDisabled()); + userEvent.hover(reimbursableButton.parentElement!); + + expect( + await findByText('Complete all required fields to continue'), + ).toBeInTheDocument(); + }); + it('disables Finish & Apply Goal on the Summary Report step when summary data is unavailable', async () => { // Empty misc constants make `buildPdsGoalConstants` return null, so // `summaryData` stays null even though every form field is valid. Without diff --git a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx index 0e116d9bd8..3f29383f3b 100644 --- a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx +++ b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import HomeIcon from '@mui/icons-material/Home'; import MenuOpenSharp from '@mui/icons-material/MenuOpenSharp'; import { ThemeProvider } from '@mui/material/styles'; -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -12,13 +12,16 @@ import { IconPanelItem, PanelLayout, PanelLayoutProps } from './PanelLayout'; const title = 'Sidebar Title'; +const iconClickSpy = jest.fn(); +const disabledIconClickSpy = jest.fn(); + const mockIcons: IconPanelItem[] = [ { key: 'mock-icon-1', icon: , label: 'Mock Icon 1', isActive: true, - onClick: jest.fn(), + onClick: iconClickSpy, }, { key: 'mock-icon-2', @@ -29,6 +32,16 @@ const mockIcons: IconPanelItem[] = [ }, ]; +const disabledIcon: IconPanelItem = { + key: 'mock-icon-disabled', + icon: , + label: 'Disabled Icon', + isActive: false, + disabled: true, + tooltip: 'Complete all required fields to continue', + onClick: disabledIconClickSpy, +}; + interface TestComponentProps extends Partial { panelType: PanelTypeEnum; } @@ -51,6 +64,10 @@ const TestComponent: React.FC = (props) => ( ); describe('PanelLayout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders main content and sidebar title for Empty panel type', () => { const { getByRole } = render( , @@ -90,7 +107,7 @@ describe('PanelLayout', () => { userEvent.click(getByRole('button', { name: 'Mock Icon 1' })); - expect(mockIcons[0].onClick).toHaveBeenCalled(); + expect(iconClickSpy).toHaveBeenCalled(); }); it('renders sidebar when content is provided', () => { @@ -225,4 +242,34 @@ describe('PanelLayout', () => { // With empty icons array, no icon buttons should render expect(queryAllByRole('button')).toHaveLength(0); }); + + it('does not call onClick when icon button is disabled', () => { + const { getByRole } = render( + , + ); + + const button = getByRole('button', { name: 'Disabled Icon' }); + expect(button).toBeDisabled(); + fireEvent.click(button); + expect(disabledIconClickSpy).not.toHaveBeenCalled(); + }); + + it('shows tooltip when hovering a disabled icon button', async () => { + const { getByRole, findByText } = render( + , + ); + + userEvent.hover( + getByRole('button', { name: 'Disabled Icon' }).parentElement!, + ); + expect( + await findByText('Complete all required fields to continue'), + ).toBeInTheDocument(); + }); }); From a60136827905a077c22ddcbd3cb7d4e2e80a4888 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Tue, 19 May 2026 09:30:57 -0500 Subject: [PATCH 3/8] Refactor PdsGoalCalculator tests to remove unnecessary waitFor calls and add validation for Setup step icon --- .../PdsGoalCalculator.test.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx index 8aa20521e2..346bbf1b32 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -165,11 +165,9 @@ describe('PdsGoalCalculator', () => { , ); - await waitFor(async () => { - expect( - await findByRole('button', { name: 'Reimbursable Expenses' }), - ).toBeDisabled(); - }); + expect( + await findByRole('button', { name: 'Reimbursable Expenses' }), + ).toBeDisabled(); expect(await findByRole('button', { name: 'Support Item' })).toBeDisabled(); expect( await findByRole('button', { name: 'Summary Report' }), @@ -183,11 +181,9 @@ describe('PdsGoalCalculator', () => { , ); - await waitFor(async () => { - expect( - await findByRole('button', { name: 'Reimbursable Expenses' }), - ).not.toBeDisabled(); - }); + expect( + await findByRole('button', { name: 'Reimbursable Expenses' }), + ).not.toBeDisabled(); expect( await findByRole('button', { name: 'Support Item' }), ).not.toBeDisabled(); @@ -196,6 +192,18 @@ describe('PdsGoalCalculator', () => { ).not.toBeDisabled(); }); + it('does not disable the Setup step icon even when Setup fields are invalid', async () => { + const { findByRole } = render( + + + , + ); + + expect( + await findByRole('button', { name: 'Calculator Setup' }), + ).not.toBeDisabled(); + }); + it('shows tooltip on disabled side panel icons', async () => { const { findByRole, findByText } = render( @@ -206,7 +214,7 @@ describe('PdsGoalCalculator', () => { const reimbursableButton = await findByRole('button', { name: 'Reimbursable Expenses', }); - await waitFor(() => expect(reimbursableButton).toBeDisabled()); + expect(reimbursableButton).toBeDisabled(); userEvent.hover(reimbursableButton.parentElement!); expect( From 6ca49d2c182c4fb10baf8d87c01442ff187c6c06 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Tue, 19 May 2026 10:08:51 -0500 Subject: [PATCH 4/8] Refactor PdsGoalCalculator to rename setup step validation to current step validation --- .../PdsGoalCalculator/Setup/SetupStep.tsx | 11 +++++++---- .../Shared/PdsGoalCalculatorContext.tsx | 16 ++++++++-------- .../Shared/PdsGoalCalculatorLayout.tsx | 5 ++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx index 837e360975..d8b325001c 100644 --- a/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx @@ -44,14 +44,17 @@ export const SetupStep: React.FC = () => { hcmUser, isMutating, setRightPanelContent, - setSetupStepValid, + setCurrentStepValid, } = usePdsGoalCalculator(); const { allValid } = useAutosaveForm(); useEffect(() => { - setSetupStepValid(allValid); - return () => setSetupStepValid(false); - }, [allValid, setSetupStepValid]); + setCurrentStepValid(allValid); + }, [allValid, setCurrentStepValid]); + + useEffect(() => { + return () => setCurrentStepValid(true); + }, [setCurrentStepValid]); const { data: userData } = useGetUserQuery(); const schema = useMemo( () => diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index 99baddec10..870914935c 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -49,9 +49,9 @@ export type PdsGoalCalculatorType = { stepIndex: number; isDrawerOpen: boolean; - /** Whether all required Setup step fields are valid */ - isSetupStepValid: boolean; - setSetupStepValid: (valid: boolean) => void; + /** Whether the current step's required fields are valid. Defaults to true. */ + isCurrentStepValid: boolean; + setCurrentStepValid: (valid: boolean) => void; handleStepChange: (stepId: PdsGoalCalculatorStepEnum) => void; handleContinue: () => void; @@ -108,7 +108,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { const steps = useSteps( calculation?.formType ?? DesignationSupportFormType.Detailed, ); - const [isSetupStepValid, setSetupStepValid] = useState(false); + const [isCurrentStepValid, setCurrentStepValid] = useState(true); const [rightPanelContent, setRightPanelContent] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(true); @@ -183,8 +183,8 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { hcmUser, rightPanelContent, isDrawerOpen, - isSetupStepValid, - setSetupStepValid, + isCurrentStepValid, + setCurrentStepValid, handleStepChange, handleContinue, handlePreviousStep, @@ -206,8 +206,8 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => { hcmUser, rightPanelContent, isDrawerOpen, - isSetupStepValid, - setSetupStepValid, + isCurrentStepValid, + setCurrentStepValid, handleStepChange, handleContinue, handlePreviousStep, diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx index 5e7bdf07f9..62e98b4db7 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorLayout.tsx @@ -19,7 +19,7 @@ export const PdsGoalCalculatorLayout: React.FC< const { steps, currentStep, - isSetupStepValid, + isCurrentStepValid, handleStepChange, isDrawerOpen, setDrawerOpen, @@ -28,8 +28,7 @@ export const PdsGoalCalculatorLayout: React.FC< calculationLoading, } = usePdsGoalCalculator(); - const navigationDisabled = - currentStep.step === PdsGoalCalculatorStepEnum.Setup && !isSetupStepValid; + const navigationDisabled = !isCurrentStepValid; const handleStepIconClick = (step: PdsGoalCalculatorStepEnum) => { if (currentStep.step === step) { From b69c3e94c8420ed585d4f0ca054e3120cfeb7924 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Wed, 20 May 2026 16:38:42 -0500 Subject: [PATCH 5/8] Refactor PdsGoalCalculator tests to simplify button enablement checks and improve readability --- .../PdsGoalCalculator.test.tsx | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx index 346bbf1b32..fa2cce0c21 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -175,33 +175,19 @@ describe('PdsGoalCalculator', () => { }); it('enables non-Setup step icons when all Setup fields are valid', async () => { - const { findByRole } = render( + const { getByRole } = render( , ); - expect( - await findByRole('button', { name: 'Reimbursable Expenses' }), - ).not.toBeDisabled(); - expect( - await findByRole('button', { name: 'Support Item' }), - ).not.toBeDisabled(); - expect( - await findByRole('button', { name: 'Summary Report' }), - ).not.toBeDisabled(); - }); - - it('does not disable the Setup step icon even when Setup fields are invalid', async () => { - const { findByRole } = render( - - - , - ); - - expect( - await findByRole('button', { name: 'Calculator Setup' }), - ).not.toBeDisabled(); + await waitFor(() => { + expect( + getByRole('button', { name: 'Reimbursable Expenses' }), + ).not.toBeDisabled(); + }); + expect(getByRole('button', { name: 'Support Item' })).not.toBeDisabled(); + expect(getByRole('button', { name: 'Summary Report' })).not.toBeDisabled(); }); it('shows tooltip on disabled side panel icons', async () => { From 9c8074fc5cff651503db1b4a5d14f59a458b7694 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Wed, 20 May 2026 16:53:11 -0500 Subject: [PATCH 6/8] Refactor PdsGoalCalculator tests to improve Summary Report button interaction and ensure it is enabled before clicking --- .../HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx index fa2cce0c21..381196495d 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -221,7 +221,11 @@ describe('PdsGoalCalculator', () => { , ); - userEvent.click(await findByRole('button', { name: 'Summary Report' })); + const summaryReportButton = await findByRole('button', { + name: 'Summary Report', + }); + await waitFor(() => expect(summaryReportButton).not.toBeDisabled()); + userEvent.click(summaryReportButton); const finishButton = await findByRole('button', { name: /Finish & Apply Goal/i, From 61f98be3868b1b8c167fcad7222961f61f50542e Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Thu, 21 May 2026 13:33:33 -0500 Subject: [PATCH 7/8] Wrap children in AutosaveForm within PdsGoalCalculatorProvider --- .../PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx index 728bdb74f4..be17e10d7f 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx @@ -6,6 +6,7 @@ import { SnackbarProvider } from 'notistack'; import { DeepPartial } from 'ts-essentials'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider, gqlMock } from '__tests__/util/graphqlMocking'; +import { AutosaveForm } from 'src/components/Shared/Autosave/AutosaveForm'; import { GetUserQuery } from 'src/components/User/GetUser.generated'; import { DesignationSupportFormType, @@ -268,7 +269,9 @@ export const PdsGoalCalculatorTestWrapper: React.FC< onCall={onCall} > {withProvider ? ( - {children} + + {children} + ) : ( // eslint-disable-next-line react/jsx-no-useless-fragment <>{children} From 086963355a5cfb5ece80a42ced494c9fbf8ef4ea Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Thu, 21 May 2026 15:26:07 -0500 Subject: [PATCH 8/8] Refactor PdsGoalCalculator tests to use aria-disabled attribute for button state checks and add a new test for side panel navigation re-enablement --- .../PdsGoalCalculator.test.tsx | 46 +++++++++++++++---- .../PanelLayout/PanelLayout.test.tsx | 13 ++---- .../PanelLayout/PanelLayout.tsx | 32 +++++++------ 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx index 381196495d..1288cd63a7 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -167,11 +167,13 @@ describe('PdsGoalCalculator', () => { expect( await findByRole('button', { name: 'Reimbursable Expenses' }), - ).toBeDisabled(); - expect(await findByRole('button', { name: 'Support Item' })).toBeDisabled(); + ).toHaveAttribute('aria-disabled', 'true'); + expect( + await findByRole('button', { name: 'Support Item' }), + ).toHaveAttribute('aria-disabled', 'true'); expect( await findByRole('button', { name: 'Summary Report' }), - ).toBeDisabled(); + ).toHaveAttribute('aria-disabled', 'true'); }); it('enables non-Setup step icons when all Setup fields are valid', async () => { @@ -184,10 +186,16 @@ describe('PdsGoalCalculator', () => { await waitFor(() => { expect( getByRole('button', { name: 'Reimbursable Expenses' }), - ).not.toBeDisabled(); + ).not.toHaveAttribute('aria-disabled', 'true'); }); - expect(getByRole('button', { name: 'Support Item' })).not.toBeDisabled(); - expect(getByRole('button', { name: 'Summary Report' })).not.toBeDisabled(); + expect(getByRole('button', { name: 'Support Item' })).not.toHaveAttribute( + 'aria-disabled', + 'true', + ); + expect(getByRole('button', { name: 'Summary Report' })).not.toHaveAttribute( + 'aria-disabled', + 'true', + ); }); it('shows tooltip on disabled side panel icons', async () => { @@ -200,14 +208,32 @@ describe('PdsGoalCalculator', () => { const reimbursableButton = await findByRole('button', { name: 'Reimbursable Expenses', }); - expect(reimbursableButton).toBeDisabled(); - userEvent.hover(reimbursableButton.parentElement!); + expect(reimbursableButton).toHaveAttribute('aria-disabled', 'true'); + userEvent.hover(reimbursableButton); expect( await findByText('Complete all required fields to continue'), ).toBeInTheDocument(); }); + it('re-enables side panel navigation after advancing from the Setup step', async () => { + const { findByRole } = render( + + + , + ); + + const continueButton = await findByRole('button', { name: 'Continue' }); + await waitFor(() => expect(continueButton).not.toBeDisabled()); + userEvent.click(continueButton); + + expect( + await findByRole('button', { name: 'Summary Report' }), + ).not.toHaveAttribute('aria-disabled', 'true'); + }); + it('disables Finish & Apply Goal on the Summary Report step when summary data is unavailable', async () => { // Empty misc constants make `buildPdsGoalConstants` return null, so // `summaryData` stays null even though every form field is valid. Without @@ -224,7 +250,9 @@ describe('PdsGoalCalculator', () => { const summaryReportButton = await findByRole('button', { name: 'Summary Report', }); - await waitFor(() => expect(summaryReportButton).not.toBeDisabled()); + await waitFor(() => + expect(summaryReportButton).not.toHaveAttribute('aria-disabled', 'true'), + ); userEvent.click(summaryReportButton); const finishButton = await findByRole('button', { diff --git a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx index 3f29383f3b..b84aec39ce 100644 --- a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx +++ b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import HomeIcon from '@mui/icons-material/Home'; import MenuOpenSharp from '@mui/icons-material/MenuOpenSharp'; import { ThemeProvider } from '@mui/material/styles'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -243,7 +243,7 @@ describe('PanelLayout', () => { expect(queryAllByRole('button')).toHaveLength(0); }); - it('does not call onClick when icon button is disabled', () => { + it('renders a disabled icon button as focusable with aria-disabled', () => { const { getByRole } = render( { ); const button = getByRole('button', { name: 'Disabled Icon' }); - expect(button).toBeDisabled(); - fireEvent.click(button); - expect(disabledIconClickSpy).not.toHaveBeenCalled(); + expect(button).toHaveAttribute('aria-disabled', 'true'); + expect(button).not.toBeDisabled(); }); it('shows tooltip when hovering a disabled icon button', async () => { @@ -265,9 +264,7 @@ describe('PanelLayout', () => { />, ); - userEvent.hover( - getByRole('button', { name: 'Disabled Icon' }).parentElement!, - ); + userEvent.hover(getByRole('button', { name: 'Disabled Icon' })); expect( await findByText('Complete all required fields to continue'), ).toBeInTheDocument(); diff --git a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx index ca57a562a0..e4c37a264f 100644 --- a/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx +++ b/src/components/HrTools/Shared/CalculationReports/PanelLayout/PanelLayout.tsx @@ -143,21 +143,23 @@ export const PanelLayout: React.FC = ({ {icons?.map((item) => ( - - - {item.icon} - - + + {item.icon} + ))} {(backHref || onBack) && (