diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx index 8449ca81cc..6f5d31751a 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx @@ -8,6 +8,7 @@ import { DateTime } from 'luxon'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { GetUserQuery } from 'src/components/User/GetUser.generated'; import theme from 'src/theme'; import { StaffSavingFundProvider } from '../../StaffSavingFund/StaffSavingFundContext'; import { @@ -65,21 +66,29 @@ const transferDefaultData: TransferModalData['transfer'] = { interface ComponentsProps { transfer?: TransferModalData['transfer']; type?: TransferTypeEnum; + lastName?: string; } const Components = ({ transfer = transferDefaultData, type, + lastName = 'Doe', }: ComponentsProps) => ( + mocks={{ + GetUser: { + user: { lastName }, + }, + }} onCall={mutationSpy} > @@ -99,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( @@ -115,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 })); @@ -125,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 }); @@ -144,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 }); @@ -167,10 +182,45 @@ describe('TransferModal', () => { ).toBeInTheDocument(); }); + it.each([ + { label: 'empty', input: '' }, + { label: 'whitespace-only', input: ' ' }, + ])( + 'should reject $label notes on one-time transfers', + async ({ input }) => { + const { getByRole, findByText } = await renderModal(); + + 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( - , - ); + const { getByRole, findByLabelText, getByLabelText, findByText } = + await renderModal(); userEvent.click(getByRole('radio', { name: /monthly/i })); expect(getByRole('radio', { name: /monthly/i })).toBeChecked(); @@ -197,11 +247,12 @@ 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 }); 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 +263,8 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.type(noteField, 'Test note'); + userEvent.click(getByRole('button', { name: /submit/i })); await waitFor(() => { @@ -225,11 +278,12 @@ 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 }); 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 +295,8 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.type(noteField, 'Test note'); + userEvent.click(submitButton); await waitFor(() => { @@ -255,7 +311,35 @@ describe('TransferModal', () => { }); describe('Inputs', () => { - it('should populate initial values from data prop', () => { + 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', 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', 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', async () => { + const { getByRole } = await renderModal({ lastName: ' ' }); + + const noteField = getByRole('textbox', { name: /note/i }); + expect(noteField).toHaveValue(''); + }); + + 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', @@ -266,16 +350,17 @@ 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'); @@ -297,7 +382,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'); @@ -326,7 +411,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 }), @@ -348,7 +433,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(); @@ -374,9 +459,10 @@ 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); @@ -391,7 +477,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 }); @@ -417,14 +503,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(); @@ -437,9 +523,10 @@ 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 }); userEvent.click(getByRole('combobox', { name: /from account/i })); userEvent.click(getByRole('option', { name: /staff account/i })); @@ -450,6 +537,9 @@ describe('TransferModal', () => { userEvent.clear(amountField); userEvent.type(amountField, '100'); + userEvent.clear(noteField); + userEvent.type(noteField, 'Test note'); + userEvent.click(getByRole('button', { name: /submit/i })); await waitFor(() => { @@ -461,7 +551,7 @@ describe('TransferModal', () => { amount: 100, sourceFundTypeName: 'Staff Account', destinationFundTypeName: 'Staff Savings', - description: '', + description: 'Test note', }), }), }), @@ -470,7 +560,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 }); @@ -532,9 +622,10 @@ 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 }); @@ -585,8 +676,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 d74adb7f92..2104ee13cd 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'; @@ -135,7 +136,11 @@ 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().required(i18n.t('Note is required')), + otherwise: (schema) => schema.nullable(), + }), }); interface TransferModalProps { data: TransferModalData; @@ -169,6 +174,19 @@ export const TransferModal: React.FC = ({ const type = data.type || TransferTypeEnum.New; const isNew = type === TransferTypeEnum.New; + 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', { + lastName: trimmedLastName, + }) + : ''; + const title = type === TransferTypeEnum.New ? t('New Fund Transfer') @@ -256,7 +274,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, }} @@ -525,11 +543,17 @@ export const TransferModal: React.FC = ({