From 9912aa451c011a8d0a41a5a615c3ec6949293493 Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Mon, 11 May 2026 14:20:56 -0700 Subject: [PATCH 1/7] Require note on savings fund transfer form --- .../TransferModal/TransferModal.test.tsx | 36 ++++++++++++++++++- .../TransferModal/TransferModal.tsx | 18 ++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index 8449ca81cc..467bd6cc6a 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -167,6 +167,31 @@ describe('TransferModal', () => { ).toBeInTheDocument(); }); + it('should require a note for one-time transfers', async () => { + const { getByRole, findByText } = render(); + + userEvent.click(getByRole('combobox', { name: /from account/i })); + userEvent.click(getByRole('option', { name: /staff account/i })); + userEvent.click(getByRole('combobox', { name: /to account/i })); + userEvent.click(getByRole('option', { name: /staff savings/i })); + + const amountField = getByRole('spinbutton', { name: /amount/i }); + userEvent.clear(amountField); + userEvent.type(amountField, '100'); + + // Note intentionally left blank. + userEvent.click(getByRole('button', { name: /submit/i })); + + expect(await findByText('Note is required')).toBeInTheDocument(); + expect(mutationSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + operation: expect.objectContaining({ + operationName: 'CreateTransfer', + }), + }), + ); + }); + it('should validate end date is after transfer date for recurring transfers', async () => { const { getByRole, findByLabelText, getByLabelText, findByText } = render( , @@ -202,6 +227,7 @@ describe('TransferModal', () => { const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); const amountField = getByRole('spinbutton', { name: /amount/i }); + const noteField = getByRole('textbox', { name: /note/i }); userEvent.click(fromAccount); userEvent.click(getByRole('option', { name: /staff account/i })); @@ -212,6 +238,8 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.type(noteField, 'Test note'); + userEvent.click(getByRole('button', { name: /submit/i })); await waitFor(() => { @@ -230,6 +258,7 @@ describe('TransferModal', () => { const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); const amountField = getByRole('spinbutton', { name: /amount/i }); + const noteField = getByRole('textbox', { name: /note/i }); const submitButton = getByRole('button', { name: /submit/i }); userEvent.click(fromAccount); @@ -241,6 +270,8 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.type(noteField, 'Test note'); + userEvent.click(submitButton); await waitFor(() => { @@ -440,6 +471,7 @@ describe('TransferModal', () => { const { getByRole } = render(); const amountField = getByRole('spinbutton', { name: /amount/i }); + const noteField = getByRole('textbox', { name: /note/i }); userEvent.click(getByRole('combobox', { name: /from account/i })); userEvent.click(getByRole('option', { name: /staff account/i })); @@ -450,6 +482,8 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.type(noteField, 'Test note'); + userEvent.click(getByRole('button', { name: /submit/i })); await waitFor(() => { @@ -461,7 +495,7 @@ describe('TransferModal', () => { amount: 100, sourceFundTypeName: 'Staff Account', destinationFundTypeName: 'Staff Savings', - description: '', + description: 'Test note', }), }), }), diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index d74adb7f92..a2a8387fe7 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -135,7 +135,15 @@ const transferSchema = (locale: string) => .number() .required(i18n.t('Amount is required')) .min(0.01, i18n.t('Amount must be at least $0.01')), - note: yup.string().nullable(), + note: yup.string().when('schedule', { + is: ScheduleEnum.OneTime, + then: (schema) => + schema + .trim() + .min(1, i18n.t('Note is required')) + .required(i18n.t('Note is required')), + otherwise: (schema) => schema.nullable(), + }), }); interface TransferModalProps { data: TransferModalData; @@ -525,11 +533,17 @@ export const TransferModal: React.FC = ({ From edda24d26392ada8856f66631d8ce0733848604a Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Tue, 12 May 2026 08:54:54 -0700 Subject: [PATCH 2/7] Default note to Savings Fund Transfer in MPDX --- .../TransferModal/TransferModal.test.tsx | 23 ++++++++++++++++++- .../TransferModal/TransferModal.tsx | 8 ++++++- .../TransfersPage/TransfersPage.tsx | 3 +++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index 467bd6cc6a..c1c8337798 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -65,11 +65,13 @@ const transferDefaultData: TransferModalData['transfer'] = { interface ComponentsProps { transfer?: TransferModalData['transfer']; type?: TransferTypeEnum; + lastName?: string; } const Components = ({ transfer = transferDefaultData, type, + lastName = 'Doe', }: ComponentsProps) => ( @@ -90,6 +92,7 @@ const Components = ({ }} funds={fundsMock} handleClose={handleClose} + lastName={lastName} /> @@ -179,7 +182,10 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); - // Note intentionally left blank. + // User clears the prefilled note before submitting. + const noteField = getByRole('textbox', { name: /note/i }); + userEvent.clear(noteField); + userEvent.click(getByRole('button', { name: /submit/i })); expect(await findByText('Note is required')).toBeInTheDocument(); @@ -286,6 +292,20 @@ describe('TransferModal', () => { }); describe('Inputs', () => { + it('should default the note to " Savings Fund Transfer in MPDX" on new one-time transfers', () => { + const { getByRole } = render(); + + const noteField = getByRole('textbox', { name: /note/i }); + expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX'); + }); + + it('should not default the note when lastName is missing', () => { + const { getByRole } = render(); + + const noteField = getByRole('textbox', { name: /note/i }); + expect(noteField).toHaveValue(''); + }); + it('should populate initial values from data prop', () => { const dataWithValues: TransferModalData['transfer'] = { transferFrom: '70056dcb-1a0f-4279-b710-928bcdff811a', @@ -482,6 +502,7 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.clear(noteField); userEvent.type(noteField, 'Test note'); userEvent.click(getByRole('button', { name: /submit/i })); diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index a2a8387fe7..5ea0ff4ccd 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -149,12 +149,14 @@ interface TransferModalProps { data: TransferModalData; funds: FundFieldsFragment[]; handleClose: () => void; + lastName?: string; } export const TransferModal: React.FC = ({ data, funds, handleClose, + lastName, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -177,6 +179,10 @@ export const TransferModal: React.FC = ({ const type = data.type || TransferTypeEnum.New; const isNew = type === TransferTypeEnum.New; + const defaultNote = lastName + ? t('{{lastName}} Savings Fund Transfer in MPDX', { lastName }) + : ''; + const title = type === TransferTypeEnum.New ? t('New Fund Transfer') @@ -264,7 +270,7 @@ export const TransferModal: React.FC = ({ status: data.transfer.status ?? '', transferDate: data.transfer.transferDate ?? getToday(), endDate: data.transfer.endDate ?? null, - note: data.transfer.note ?? '', + note: data.transfer.note || defaultNote, isEditing: Boolean(data.transfer.id), originalStart: data.transfer.transferDate ?? null, }} diff --git a/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx b/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx index 3aaf9308af..9238641ecc 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx @@ -24,6 +24,7 @@ import { MultiPageHeader, } from 'src/components/Shared/MultiPageLayout/MultiPageHeader'; import { useStaffAccountQuery } from 'src/components/Shared/StaffAccount/StaffAccount.generated'; +import { useGetUserQuery } from 'src/components/User/GetUser.generated'; import theme from 'src/theme'; import { StaffSavingFundContext, @@ -84,6 +85,7 @@ export const TransfersPage: React.FC = ({ title }) => { const { data: staffAccountData, error: staffAccountError } = useStaffAccountQuery(); + const { data: userData } = useGetUserQuery(); const { data: reportData, loading: reportLoading } = useReportsSavingsFundTransferQuery(); @@ -342,6 +344,7 @@ export const TransfersPage: React.FC = ({ title }) => { handleClose={() => setModalData(null)} data={modalData} funds={funds} + lastName={userData?.user?.lastName ?? undefined} /> )} From 417b0af642d7eb5bb6cebae3add0e815b46a051d Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Tue, 12 May 2026 10:17:07 -0700 Subject: [PATCH 3/7] Trim lastName and lock in whitespace-only note rejection --- .../TransferModal/TransferModal.test.tsx | 42 +++++++++++++++++++ .../TransferModal/TransferModal.tsx | 7 +++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index c1c8337798..6bd60d4005 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -198,6 +198,34 @@ describe('TransferModal', () => { ); }); + it('should reject whitespace-only notes on one-time transfers', async () => { + const { getByRole, findByText } = render(); + + userEvent.click(getByRole('combobox', { name: /from account/i })); + userEvent.click(getByRole('option', { name: /staff account/i })); + userEvent.click(getByRole('combobox', { name: /to account/i })); + userEvent.click(getByRole('option', { name: /staff savings/i })); + + const amountField = getByRole('spinbutton', { name: /amount/i }); + userEvent.clear(amountField); + userEvent.type(amountField, '100'); + + const noteField = getByRole('textbox', { name: /note/i }); + userEvent.clear(noteField); + userEvent.type(noteField, ' '); + + userEvent.click(getByRole('button', { name: /submit/i })); + + expect(await findByText('Note is required')).toBeInTheDocument(); + expect(mutationSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + operation: expect.objectContaining({ + operationName: 'CreateTransfer', + }), + }), + ); + }); + it('should validate end date is after transfer date for recurring transfers', async () => { const { getByRole, findByLabelText, getByLabelText, findByText } = render( , @@ -306,6 +334,20 @@ describe('TransferModal', () => { expect(noteField).toHaveValue(''); }); + it('should trim lastName when building the default note', () => { + const { getByRole } = render(); + + const noteField = getByRole('textbox', { name: /note/i }); + expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX'); + }); + + it('should not default the note when lastName is only whitespace', () => { + const { getByRole } = render(); + + const noteField = getByRole('textbox', { name: /note/i }); + expect(noteField).toHaveValue(''); + }); + it('should populate initial values from data prop', () => { const dataWithValues: TransferModalData['transfer'] = { transferFrom: '70056dcb-1a0f-4279-b710-928bcdff811a', diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index 5ea0ff4ccd..6c1c50d407 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -179,8 +179,11 @@ export const TransferModal: React.FC = ({ const type = data.type || TransferTypeEnum.New; const isNew = type === TransferTypeEnum.New; - const defaultNote = lastName - ? t('{{lastName}} Savings Fund Transfer in MPDX', { lastName }) + const trimmedLastName = lastName?.trim(); + const defaultNote = trimmedLastName + ? t('{{lastName}} Savings Fund Transfer in MPDX', { + lastName: trimmedLastName, + }) : ''; const title = From b05637ed5c7d5c7c00072d835ceeb0bc410cadd7 Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Tue, 12 May 2026 10:35:53 -0700 Subject: [PATCH 4/7] Consolidate empty/whitespace note tests with it.each --- .../TransferModal/TransferModal.test.tsx | 88 +++++++------------ 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index 6bd60d4005..0da21fd67f 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -170,61 +170,41 @@ describe('TransferModal', () => { ).toBeInTheDocument(); }); - it('should require a note for one-time transfers', async () => { - const { getByRole, findByText } = render(); - - userEvent.click(getByRole('combobox', { name: /from account/i })); - userEvent.click(getByRole('option', { name: /staff account/i })); - userEvent.click(getByRole('combobox', { name: /to account/i })); - userEvent.click(getByRole('option', { name: /staff savings/i })); - - const amountField = getByRole('spinbutton', { name: /amount/i }); - userEvent.clear(amountField); - userEvent.type(amountField, '100'); - - // User clears the prefilled note before submitting. - const noteField = getByRole('textbox', { name: /note/i }); - userEvent.clear(noteField); - - userEvent.click(getByRole('button', { name: /submit/i })); - - expect(await findByText('Note is required')).toBeInTheDocument(); - expect(mutationSpy).not.toHaveBeenCalledWith( - expect.objectContaining({ - operation: expect.objectContaining({ - operationName: 'CreateTransfer', - }), - }), - ); - }); - - it('should reject whitespace-only notes on one-time transfers', async () => { - const { getByRole, findByText } = render(); - - userEvent.click(getByRole('combobox', { name: /from account/i })); - userEvent.click(getByRole('option', { name: /staff account/i })); - userEvent.click(getByRole('combobox', { name: /to account/i })); - userEvent.click(getByRole('option', { name: /staff savings/i })); - - const amountField = getByRole('spinbutton', { name: /amount/i }); - userEvent.clear(amountField); - userEvent.type(amountField, '100'); - - const noteField = getByRole('textbox', { name: /note/i }); - userEvent.clear(noteField); - userEvent.type(noteField, ' '); - - userEvent.click(getByRole('button', { name: /submit/i })); - - expect(await findByText('Note is required')).toBeInTheDocument(); - expect(mutationSpy).not.toHaveBeenCalledWith( - expect.objectContaining({ - operation: expect.objectContaining({ - operationName: 'CreateTransfer', + it.each([ + { label: 'empty', input: '' }, + { label: 'whitespace-only', input: ' ' }, + ])( + 'should reject $label notes on one-time transfers', + async ({ input }) => { + const { getByRole, findByText } = render(); + + userEvent.click(getByRole('combobox', { name: /from account/i })); + userEvent.click(getByRole('option', { name: /staff account/i })); + userEvent.click(getByRole('combobox', { name: /to account/i })); + userEvent.click(getByRole('option', { name: /staff savings/i })); + + const amountField = getByRole('spinbutton', { name: /amount/i }); + userEvent.clear(amountField); + userEvent.type(amountField, '100'); + + const noteField = getByRole('textbox', { name: /note/i }); + userEvent.clear(noteField); + if (input) { + userEvent.type(noteField, input); + } + + userEvent.click(getByRole('button', { name: /submit/i })); + + expect(await findByText('Note is required')).toBeInTheDocument(); + expect(mutationSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + operation: expect.objectContaining({ + operationName: 'CreateTransfer', + }), }), - }), - ); - }); + ); + }, + ); it('should validate end date is after transfer date for recurring transfers', async () => { const { getByRole, findByLabelText, getByLabelText, findByText } = render( From 6f5c546d9e52bc69e670b348d272c11ec0a8f48b Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Tue, 12 May 2026 15:41:30 -0700 Subject: [PATCH 5/7] Move user query into modal, drop redundant min --- .../TransferModal/TransferModal.test.tsx | 29 ++++++++++++++++++- .../TransferModal/TransferModal.tsx | 12 +++----- .../TransfersPage/TransfersPage.tsx | 3 -- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index 0da21fd67f..f7c8f048a9 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -8,6 +8,12 @@ import { DateTime } from 'luxon'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { + GetUserDocument, + GetUserQuery, +} from 'src/components/User/GetUser.generated'; +import { UserTypeEnum } from 'src/graphql/types.generated'; +import { createCache } from 'src/lib/apollo/cache'; import theme from 'src/theme'; import { StaffSavingFundProvider } from '../../StaffSavingFund/StaffSavingFundContext'; import { @@ -68,6 +74,27 @@ interface ComponentsProps { lastName?: string; } +const buildCacheWithUser = (lastName: string) => { + const cache = createCache(); + cache.writeQuery({ + query: GetUserDocument, + data: { + user: { + __typename: 'User', + id: 'test-user-id', + firstName: '', + lastName, + avatar: '', + staffAccountId: null, + primaryDesignation: null, + userType: UserTypeEnum.UsStaff, + preferences: null, + }, + }, + }); + return cache; +}; + const Components = ({ transfer = transferDefaultData, type, @@ -82,6 +109,7 @@ const Components = ({ createTransfer: CreateTransferMutation; updateRecurringTransfer: UpdateRecurringTransferMutation; }> + cache={buildCacheWithUser(lastName)} onCall={mutationSpy} > @@ -92,7 +120,6 @@ const Components = ({ }} funds={fundsMock} handleClose={handleClose} - lastName={lastName} /> diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index 6c1c50d407..607bef30c3 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -30,6 +30,7 @@ import { SubmitButton, } from 'src/components/Shared/Modal/ActionButtons/ActionButtons'; import Modal from 'src/components/Shared/Modal/Modal'; +import { useGetUserQuery } from 'src/components/User/GetUser.generated'; import { useLocale } from 'src/hooks/useLocale'; import i18n from 'src/lib/i18n'; import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; @@ -137,11 +138,7 @@ const transferSchema = (locale: string) => .min(0.01, i18n.t('Amount must be at least $0.01')), note: yup.string().when('schedule', { is: ScheduleEnum.OneTime, - then: (schema) => - schema - .trim() - .min(1, i18n.t('Note is required')) - .required(i18n.t('Note is required')), + then: (schema) => schema.trim().required(i18n.t('Note is required')), otherwise: (schema) => schema.nullable(), }), }); @@ -149,14 +146,12 @@ interface TransferModalProps { data: TransferModalData; funds: FundFieldsFragment[]; handleClose: () => void; - lastName?: string; } export const TransferModal: React.FC = ({ data, funds, handleClose, - lastName, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -179,7 +174,8 @@ export const TransferModal: React.FC = ({ const type = data.type || TransferTypeEnum.New; const isNew = type === TransferTypeEnum.New; - const trimmedLastName = lastName?.trim(); + const { data: userData } = useGetUserQuery(); + const trimmedLastName = userData?.user?.lastName?.trim(); const defaultNote = trimmedLastName ? t('{{lastName}} Savings Fund Transfer in MPDX', { lastName: trimmedLastName, diff --git a/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx b/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx index 9238641ecc..3aaf9308af 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransfersPage/TransfersPage.tsx @@ -24,7 +24,6 @@ import { MultiPageHeader, } from 'src/components/Shared/MultiPageLayout/MultiPageHeader'; import { useStaffAccountQuery } from 'src/components/Shared/StaffAccount/StaffAccount.generated'; -import { useGetUserQuery } from 'src/components/User/GetUser.generated'; import theme from 'src/theme'; import { StaffSavingFundContext, @@ -85,7 +84,6 @@ export const TransfersPage: React.FC = ({ title }) => { const { data: staffAccountData, error: staffAccountError } = useStaffAccountQuery(); - const { data: userData } = useGetUserQuery(); const { data: reportData, loading: reportLoading } = useReportsSavingsFundTransferQuery(); @@ -344,7 +342,6 @@ export const TransfersPage: React.FC = ({ title }) => { handleClose={() => setModalData(null)} data={modalData} funds={funds} - lastName={userData?.user?.lastName ?? undefined} /> )} From fcda99171e02ee31e4351fd20173f391ea72806c Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Thu, 14 May 2026 10:01:04 -0700 Subject: [PATCH 6/7] Use mocks instead of cache; guard modal while loading --- .../TransferModal/TransferModal.test.tsx | 118 +++++++----------- .../TransferModal/TransferModal.tsx | 7 +- 2 files changed, 54 insertions(+), 71 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index f7c8f048a9..aad59597c0 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -8,12 +8,7 @@ import { DateTime } from 'luxon'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { - GetUserDocument, - GetUserQuery, -} from 'src/components/User/GetUser.generated'; -import { UserTypeEnum } from 'src/graphql/types.generated'; -import { createCache } from 'src/lib/apollo/cache'; +import { GetUserQuery } from 'src/components/User/GetUser.generated'; import theme from 'src/theme'; import { StaffSavingFundProvider } from '../../StaffSavingFund/StaffSavingFundContext'; import { @@ -74,27 +69,6 @@ interface ComponentsProps { lastName?: string; } -const buildCacheWithUser = (lastName: string) => { - const cache = createCache(); - cache.writeQuery({ - query: GetUserDocument, - data: { - user: { - __typename: 'User', - id: 'test-user-id', - firstName: '', - lastName, - avatar: '', - staffAccountId: null, - primaryDesignation: null, - userType: UserTypeEnum.UsStaff, - preferences: null, - }, - }, - }); - return cache; -}; - const Components = ({ transfer = transferDefaultData, type, @@ -105,11 +79,16 @@ const Components = ({ - cache={buildCacheWithUser(lastName)} + mocks={{ + GetUser: { + user: { lastName }, + }, + }} onCall={mutationSpy} > @@ -129,9 +108,15 @@ const Components = ({ ); +const renderModal = async (props: ComponentsProps = {}) => { + const result = render(); + await result.findByText(/Fund Transfer/); + return result; +}; + describe('TransferModal', () => { - it('should render the modal with correct inputs', () => { - const { getByRole, getByText } = render(); + it('should render the modal with correct inputs', async () => { + const { getByRole, getByText } = await renderModal(); expect(getByText('New Fund Transfer')).toBeInTheDocument(); expect( @@ -145,8 +130,8 @@ describe('TransferModal', () => { expect(getByRole('spinbutton', { name: /amount/i })).toBeInTheDocument(); }); - it('should close modal when cancel button is clicked', () => { - const { getByRole } = render(); + it('should close modal when cancel button is clicked', async () => { + const { getByRole } = await renderModal(); userEvent.click(getByRole('button', { name: /cancel/i })); @@ -155,7 +140,7 @@ describe('TransferModal', () => { describe('Handle submit and validation', () => { it('should show validation errors for required fields', async () => { - const { getByRole, findByText } = render(); + const { getByRole, findByText } = await renderModal(); const toAccount = getByRole('combobox', { name: /to account/i }); const amountField = getByRole('spinbutton', { name: /amount/i }); @@ -174,7 +159,7 @@ describe('TransferModal', () => { }); it('should validate amount is greater than $0.01', async () => { - const { getByRole, findByText } = render(); + const { getByRole, findByText } = await renderModal(); const amountField = getByRole('spinbutton', { name: /amount/i }); @@ -203,7 +188,7 @@ describe('TransferModal', () => { ])( 'should reject $label notes on one-time transfers', async ({ input }) => { - const { getByRole, findByText } = render(); + const { getByRole, findByText } = await renderModal(); userEvent.click(getByRole('combobox', { name: /from account/i })); userEvent.click(getByRole('option', { name: /staff account/i })); @@ -234,9 +219,8 @@ describe('TransferModal', () => { ); it('should validate end date is after transfer date for recurring transfers', async () => { - const { getByRole, findByLabelText, getByLabelText, findByText } = render( - , - ); + const { getByRole, findByLabelText, getByLabelText, findByText } = + await renderModal(); userEvent.click(getByRole('radio', { name: /monthly/i })); expect(getByRole('radio', { name: /monthly/i })).toBeChecked(); @@ -263,7 +247,7 @@ describe('TransferModal', () => { }); it('should submit form with valid data', async () => { - const { getByRole } = render(); + const { getByRole } = await renderModal(); const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); @@ -294,7 +278,7 @@ describe('TransferModal', () => { }); it('should handle form submission successfully', async () => { - const { getByRole } = render(); + const { getByRole } = await renderModal(); const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); @@ -327,35 +311,35 @@ describe('TransferModal', () => { }); describe('Inputs', () => { - it('should default the note to " Savings Fund Transfer in MPDX" on new one-time transfers', () => { - const { getByRole } = render(); + it('should default the note to " Savings Fund Transfer in MPDX" on new one-time transfers', async () => { + const { getByRole } = await renderModal({ lastName: 'Sleight' }); const noteField = getByRole('textbox', { name: /note/i }); expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX'); }); - it('should not default the note when lastName is missing', () => { - const { getByRole } = render(); + it('should not default the note when lastName is missing', async () => { + const { getByRole } = await renderModal({ lastName: '' }); const noteField = getByRole('textbox', { name: /note/i }); expect(noteField).toHaveValue(''); }); - it('should trim lastName when building the default note', () => { - const { getByRole } = render(); + it('should trim lastName when building the default note', async () => { + const { getByRole } = await renderModal({ lastName: ' Sleight ' }); const noteField = getByRole('textbox', { name: /note/i }); expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX'); }); - it('should not default the note when lastName is only whitespace', () => { - const { getByRole } = render(); + it('should not default the note when lastName is only whitespace', async () => { + const { getByRole } = await renderModal({ lastName: ' ' }); const noteField = getByRole('textbox', { name: /note/i }); expect(noteField).toHaveValue(''); }); - it('should populate initial values from data prop', () => { + it('should populate initial values from data prop', async () => { const dataWithValues: TransferModalData['transfer'] = { transferFrom: '70056dcb-1a0f-4279-b710-928bcdff811a', transferTo: '408caf15-cdfd-41d1-8778-aa42a6561b85', @@ -366,16 +350,14 @@ describe('TransferModal', () => { note: 'Test note', }; - const { getByDisplayValue, getByLabelText } = render( - , - ); + const { getByDisplayValue, getByLabelText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); expect(getByDisplayValue('500')).toBeInTheDocument(); expect(getByLabelText(/end date/i)).toHaveValue(''); }); it('should validate that from and to accounts are different', async () => { - const { getByRole, queryByRole, getAllByRole } = render(); + const { getByRole, queryByRole, getAllByRole } = await renderModal(); const [fromAccount, toAccount] = getAllByRole('combobox'); @@ -397,7 +379,7 @@ describe('TransferModal', () => { }); it('should swap accounts when swap button is clicked', async () => { - const { getByRole, getByTestId } = render(); + const { getByRole, getByTestId } = await renderModal(); const icon = getByTestId('SwapHorizIcon'); const swapButton = icon.closest('button'); @@ -426,7 +408,7 @@ describe('TransferModal', () => { }); it('should show/hide end date based on schedule selection', async () => { - const { getByRole, queryByRole, getByLabelText } = render(); + const { getByRole, queryByRole, getByLabelText } = await renderModal(); expect( queryByRole('textbox', { name: /end date/i }), @@ -448,7 +430,7 @@ describe('TransferModal', () => { }); it('should show error message when monthly schedule is selected', async () => { - const { getByRole, getByLabelText, findByText } = render(); + const { getByRole, getByLabelText, findByText } = await renderModal(); userEvent.click(getByRole('radio', { name: /monthly/i })); expect(getByRole('radio', { name: /monthly/i })).toBeChecked(); @@ -474,9 +456,7 @@ describe('TransferModal', () => { note: 'Test note', }; - const { getByLabelText, findByText } = render( - , - ); + const { getByLabelText, findByText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); const transferDate = getByLabelText(/transfer date/i); @@ -491,7 +471,7 @@ describe('TransferModal', () => { }); it('should show information box when amount exceeds limit', async () => { - const { getByRole, findByRole } = render(); + const { getByRole, findByRole } = await renderModal(); const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); @@ -517,14 +497,14 @@ describe('TransferModal', () => { ).toBeInTheDocument(); }); - it('should show proper currency symbol in amount field', () => { - const { getByText } = render(); + it('should show proper currency symbol in amount field', async () => { + const { getByText } = await renderModal(); expect(getByText('$')).toBeInTheDocument(); }); it('should handle different schedule types', async () => { - const { getByRole } = render(); + const { getByRole } = await renderModal(); expect(getByRole('radio', { name: /one time/i })).toBeChecked(); @@ -537,7 +517,7 @@ describe('TransferModal', () => { describe('Mutations', () => { it('should create a one-time transfer', async () => { - const { getByRole } = render(); + const { getByRole } = await renderModal(); const amountField = getByRole('spinbutton', { name: /amount/i }); const noteField = getByRole('textbox', { name: /note/i }); @@ -574,7 +554,7 @@ describe('TransferModal', () => { }); it('should create a recurring transfer', async () => { - const { getByRole, getByLabelText } = render(); + const { getByRole, getByLabelText } = await renderModal(); const amountField = getByRole('spinbutton', { name: /amount/i }); @@ -636,9 +616,7 @@ describe('TransferModal', () => { recurringId: 'recurring-id', }; - const { getByRole, getByLabelText } = render( - , - ); + const { getByRole, getByLabelText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); const amountField = getByRole('spinbutton', { name: /amount/i }); @@ -689,8 +667,8 @@ describe('TransferModal', () => { }); describe('New Mode', () => { - it('should display selects', () => { - const { getByRole } = render(); + it('should display selects', async () => { + const { getByRole } = await renderModal({ type: TransferTypeEnum.New }); const fromAccount = getByRole('combobox', { name: /from account/i }); const toAccount = getByRole('combobox', { name: /to account/i }); diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index 607bef30c3..2104ee13cd 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -174,7 +174,12 @@ export const TransferModal: React.FC = ({ const type = data.type || TransferTypeEnum.New; const isNew = type === TransferTypeEnum.New; - const { data: userData } = useGetUserQuery(); + const { data: userData, loading: userLoading } = useGetUserQuery(); + + if (userLoading) { + return null; + } + const trimmedLastName = userData?.user?.lastName?.trim(); const defaultNote = trimmedLastName ? t('{{lastName}} Savings Fund Transfer in MPDX', { From 2ae7c7daf5f498d82de3476487198ebf47651a18 Mon Sep 17 00:00:00 2001 From: Jeff Clay <91073628+jeff-clay@users.noreply.github.com> Date: Thu, 14 May 2026 10:17:04 -0700 Subject: [PATCH 7/7] Run prettier on test file --- .../TransferModal/TransferModal.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index aad59597c0..6f5d31751a 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -350,7 +350,10 @@ describe('TransferModal', () => { note: 'Test note', }; - const { getByDisplayValue, getByLabelText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); + const { getByDisplayValue, getByLabelText } = await renderModal({ + transfer: dataWithValues, + type: TransferTypeEnum.Edit, + }); expect(getByDisplayValue('500')).toBeInTheDocument(); expect(getByLabelText(/end date/i)).toHaveValue(''); @@ -456,7 +459,10 @@ describe('TransferModal', () => { note: 'Test note', }; - const { getByLabelText, findByText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); + const { getByLabelText, findByText } = await renderModal({ + transfer: dataWithValues, + type: TransferTypeEnum.Edit, + }); const transferDate = getByLabelText(/transfer date/i); @@ -616,7 +622,10 @@ describe('TransferModal', () => { recurringId: 'recurring-id', }; - const { getByRole, getByLabelText } = await renderModal({ transfer: dataWithValues, type: TransferTypeEnum.Edit }); + const { getByRole, getByLabelText } = await renderModal({ + transfer: dataWithValues, + type: TransferTypeEnum.Edit, + }); const amountField = getByRole('spinbutton', { name: /amount/i });