From 4ed922c35617db514514872111c1b51da27b6c67 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Wed, 1 Jul 2026 14:45:47 -0600 Subject: [PATCH] fix: add manual account tests --- .../ManualAccountConnect-test.tsx | 242 ++++++++++++++++ .../manualAccount/ManualAccountForm-test.tsx | 274 ++++++++++++++++++ 2 files changed, 516 insertions(+) create mode 100644 src/views/manualAccount/ManualAccountConnect-test.tsx create mode 100644 src/views/manualAccount/ManualAccountForm-test.tsx diff --git a/src/views/manualAccount/ManualAccountConnect-test.tsx b/src/views/manualAccount/ManualAccountConnect-test.tsx new file mode 100644 index 0000000000..9d43d25dc4 --- /dev/null +++ b/src/views/manualAccount/ManualAccountConnect-test.tsx @@ -0,0 +1,242 @@ +import React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { type UserEvent } from '@testing-library/user-event' +import { render, screen } from 'src/utilities/testingLibrary' +import RenderConnectStep from 'src/components/RenderConnectStep' +import { ConnectNavigationHeader } from 'src/components/ConnectNavigationHeader' +import { initialState, institutionData, member } from 'src/services/mockedData' +import { apiValue as baseApiValue } from 'src/const/apiProviderMock' +import { PostMessageContext } from 'src/ConnectWidget' +import { AccountTypes } from 'src/views/manualAccount/constants' +import { STEPS } from 'src/const/Connect' + +type NavigationHandle = { + handleBackButton: () => void + showBackButton: () => HTMLElement | false | null +} + +type RenderStepOptions = { + apiOverrides?: Partial + availableAccountTypes?: number[] + onManualAccountAdded?: ReturnType +} + +const manualAccountState = { + ...initialState, + connect: { + ...initialState.connect, + location: [{ step: STEPS.ADD_MANUAL_ACCOUNT }], + }, +} + +const buildMockApi = (apiOverrides: Partial = {}) => ({ + ...baseApiValue, + createAccount: vi.fn( + (): Promise => + Promise.resolve({ + member_guid: member.member.guid, + institution_guid: institutionData.institution.guid, + } as unknown as AccountResponseType), + ), + ...apiOverrides, +}) + +const renderManualAccountStep = ({ + apiOverrides = {}, + availableAccountTypes = [AccountTypes.CHECKING, AccountTypes.SAVINGS], + onManualAccountAdded = vi.fn(), +}: RenderStepOptions = {}) => { + const onPostMessage = vi.fn() + const mockApi = buildMockApi(apiOverrides) + + return { + ...render( + + {}} + handleCredentialsGoBack={() => {}} + navigationRef={React.createRef()} + onManualAccountAdded={onManualAccountAdded} + onUpsertMember={() => {}} + setConnectLocalState={() => {}} + /> + , + { apiValue: mockApi, preloadedState: manualAccountState }, + ), + mockApi, + onManualAccountAdded, + onPostMessage, + } +} + +const ManualAccountWithNavigationHeader = ({ + availableAccountTypes, + onManualAccountAdded, +}: { + availableAccountTypes: number[] + onManualAccountAdded: () => void +}) => { + const [stepComponentRef, setStepComponentRef] = React.useState(null) + + return ( + <> + {}} stepComponentRef={stepComponentRef} /> + {}} + handleCredentialsGoBack={() => {}} + navigationRef={setStepComponentRef} + onManualAccountAdded={onManualAccountAdded} + onUpsertMember={() => {}} + setConnectLocalState={() => {}} + /> + + ) +} + +const renderWithNavigationHeader = ({ + apiOverrides = {}, + availableAccountTypes = [AccountTypes.CHECKING, AccountTypes.SAVINGS], + onManualAccountAdded = vi.fn(), +}: RenderStepOptions = {}) => { + const onPostMessage = vi.fn() + const mockApi = buildMockApi(apiOverrides) + + return { + ...render( + + + , + { apiValue: mockApi, preloadedState: manualAccountState }, + ), + mockApi, + onManualAccountAdded, + onPostMessage, + } +} + +const addManualCheckingAccount = async (user: UserEvent) => { + await user.click(await screen.findByRole('button', { name: 'Checking' })) + await user.type(await screen.findByTestId('text-input-user_name'), 'My Checking') + await user.click(screen.getByTestId('save-manual-account-button')) +} + +describe('', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Content Display', () => { + it('renders the manual account menu with the available account types', async () => { + renderManualAccountStep() + + expect(await screen.findByTestId('manual-account-menu-container')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Checking' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Savings' })).toBeInTheDocument() + }) + + it('shows only the provided account type when a single type is available', async () => { + renderManualAccountStep({ availableAccountTypes: [AccountTypes.CHECKING] }) + + expect(await screen.findByRole('button', { name: 'Checking' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Savings' })).not.toBeInTheDocument() + }) + }) + + describe('Account Type Selection', () => { + it('opens the manual account form for the selected account type', async () => { + const { user } = renderManualAccountStep() + + await user.click(await screen.findByRole('button', { name: 'Checking' })) + + expect(await screen.findByTestId('manual-account-form-header')).toHaveTextContent('Checking') + expect(screen.queryByTestId('manual-account-menu-container')).not.toBeInTheDocument() + }) + + it('opens the form for a different account type', async () => { + const { user } = renderManualAccountStep() + + await user.click(await screen.findByRole('button', { name: 'Savings' })) + + expect(await screen.findByTestId('manual-account-form-header')).toHaveTextContent('Savings') + }) + }) + + describe('Success Flow', () => { + it('creates the account and shows the success view after saving', async () => { + const { user, mockApi } = renderManualAccountStep() + + await addManualCheckingAccount(user) + + expect(await screen.findByTestId('manual-account-success-header')).toHaveTextContent( + 'Checking added', + ) + expect(mockApi.createAccount).toHaveBeenCalledWith( + expect.objectContaining({ + account_type: AccountTypes.CHECKING, + user_name: 'My Checking', + }), + ) + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + }) + + it('shows an error when account creation fails', async () => { + const { user } = renderManualAccountStep({ + apiOverrides: { + createAccount: vi.fn().mockRejectedValue(new Error('Failed to create account')), + }, + }) + + await addManualCheckingAccount(user) + + expect(await screen.findByTestId('something-went-wrong-text')).toBeInTheDocument() + expect(screen.queryByTestId('manual-account-success-header')).not.toBeInTheDocument() + }) + + it('posts back to search when Done is clicked on the success view', async () => { + const { user } = renderManualAccountStep() + + await addManualCheckingAccount(user) + + await user.click(await screen.findByTestId('manual-success-done-button')) + expect(await screen.findByTestId('search-header')).toBeInTheDocument() + }) + }) + + describe('Navigation back button', () => { + it('returns from the form to the menu when the back button is clicked', async () => { + const { user } = renderWithNavigationHeader() + + await user.click(await screen.findByRole('button', { name: 'Checking' })) + expect(await screen.findByTestId('manual-account-form-header')).toBeInTheDocument() + + await user.click(await screen.findByTestId('back-button')) + + expect(await screen.findByTestId('manual-account-menu-container')).toBeInTheDocument() + expect(screen.queryByTestId('manual-account-form-header')).not.toBeInTheDocument() + }) + + it('returns to search when the back button is clicked from the menu', async () => { + const { user } = renderWithNavigationHeader() + + await user.click(await screen.findByTestId('back-button')) + + expect(await screen.findByTestId('search-header')).toBeInTheDocument() + }) + + it('hides the back button on the success view', async () => { + const { user } = renderWithNavigationHeader() + + expect(await screen.findByTestId('back-button')).toBeInTheDocument() + + await addManualCheckingAccount(user) + await screen.findByTestId('manual-account-success-header') + + expect(screen.queryByTestId('back-button')).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/views/manualAccount/ManualAccountForm-test.tsx b/src/views/manualAccount/ManualAccountForm-test.tsx new file mode 100644 index 0000000000..302401df62 --- /dev/null +++ b/src/views/manualAccount/ManualAccountForm-test.tsx @@ -0,0 +1,274 @@ +import React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import RenderConnectStep from 'src/components/RenderConnectStep' +import { initialState, institutionData, member } from 'src/services/mockedData' +import { apiValue as baseApiValue } from 'src/const/apiProviderMock' +import { PostMessageContext } from 'src/ConnectWidget' +import { AccountTypes } from 'src/views/manualAccount/constants' +import { STEPS } from 'src/const/Connect' + +type RenderFormOptions = { + accountType?: number + apiOverrides?: Partial + preloadedMembers?: Array<{ institution_guid: string }> +} + +const accountTypeButtonName: Record = { + [AccountTypes.CHECKING]: 'Checking', + [AccountTypes.SAVINGS]: 'Savings', + [AccountTypes.LOAN]: 'Loan', + [AccountTypes.CREDIT_CARD]: 'Credit Card', +} + +const buildMockApi = (apiOverrides: Partial = {}) => ({ + ...baseApiValue, + createAccount: vi.fn( + (): Promise => + Promise.resolve({ + member_guid: member.member.guid, + institution_guid: institutionData.institution.guid, + } as unknown as AccountResponseType), + ), + ...apiOverrides, +}) + +const renderManualAccountForm = async ({ + accountType = AccountTypes.CHECKING, + apiOverrides = {}, + preloadedMembers = [], +}: RenderFormOptions = {}) => { + const mockApi = buildMockApi(apiOverrides) + + const utils = render( + + {}} + handleCredentialsGoBack={() => {}} + navigationRef={React.createRef()} + onManualAccountAdded={() => {}} + onUpsertMember={() => {}} + setConnectLocalState={() => {}} + /> + , + { + apiValue: mockApi, + preloadedState: { + ...initialState, + connect: { + ...initialState.connect, + location: [{ step: STEPS.ADD_MANUAL_ACCOUNT }], + members: preloadedMembers, + }, + }, + }, + ) + + await utils.user.click( + await screen.findByRole('button', { name: accountTypeButtonName[accountType] }), + ) + await screen.findByTestId('manual-account-form-header') + + return { ...utils, mockApi } +} + +describe('', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Content Display', () => { + it('renders the checking account form fields', async () => { + await renderManualAccountForm() + + expect(screen.getByTestId('manual-account-form-header')).toHaveTextContent('Checking') + expect(screen.getByLabelText(/account name/i)).toBeInTheDocument() + expect(screen.getByLabelText(/account balance/i)).toBeInTheDocument() + expect(screen.getByText(/required/i)).toBeInTheDocument() + expect(screen.getByTestId('save-manual-account-button')).toHaveTextContent('Save') + expect(screen.queryByLabelText(/credit limit/i)).not.toBeInTheDocument() + }) + + it('renders the header for the selected account type', async () => { + await renderManualAccountForm({ accountType: AccountTypes.SAVINGS }) + + expect(screen.getByTestId('manual-account-form-header')).toHaveTextContent('Savings') + }) + }) + + describe('Account Type Specific Fields', () => { + it('renders the credit card specific fields', async () => { + await renderManualAccountForm({ accountType: AccountTypes.CREDIT_CARD }) + + expect(screen.getByLabelText(/credit limit/i)).toBeInTheDocument() + expect(screen.getByLabelText(/minimum payment/i)).toBeInTheDocument() + expect(screen.getByLabelText(/day of the month payment is due/i)).toBeInTheDocument() + }) + + it('renders the interest rate field for loan accounts', async () => { + await renderManualAccountForm({ accountType: AccountTypes.LOAN }) + + expect(screen.getByLabelText(/interest rate/i)).toBeInTheDocument() + }) + }) + + describe('Personal and Business Selection', () => { + it('selects personal by default', async () => { + await renderManualAccountForm() + + expect(screen.getByLabelText('Personal')).toBeChecked() + expect(screen.getByLabelText('Business')).not.toBeChecked() + }) + + it('toggles between personal and business', async () => { + const { user } = await renderManualAccountForm() + + await user.click(screen.getByLabelText('Business')) + + expect(screen.getByLabelText('Business')).toBeChecked() + expect(screen.getByLabelText('Personal')).not.toBeChecked() + + await user.click(screen.getByLabelText('Personal')) + + expect(screen.getByLabelText('Personal')).toBeChecked() + expect(screen.getByLabelText('Business')).not.toBeChecked() + }) + }) + + describe('Day Picker', () => { + it('opens the day picker and returns to the form when the back button is clicked', async () => { + const { user } = await renderManualAccountForm({ accountType: AccountTypes.CREDIT_CARD }) + + await user.click(screen.getByLabelText(/day of the month payment is due/i)) + + expect(await screen.findByText('Payment due day')).toBeInTheDocument() + expect(screen.queryByTestId('manual-account-form-header')).not.toBeInTheDocument() + + await user.click(screen.getByTestId('back-button')) + + expect(await screen.findByTestId('manual-account-form-header')).toBeInTheDocument() + }) + + it('selects a day and returns to the form', async () => { + const { user } = await renderManualAccountForm({ accountType: AccountTypes.CREDIT_CARD }) + + await user.click(screen.getByLabelText(/day of the month payment is due/i)) + await user.click(await screen.findByTestId('date-picker-button-15')) + + expect(await screen.findByTestId('manual-account-form-header')).toBeInTheDocument() + expect(screen.getByLabelText(/day of the month payment is due/i)).toHaveValue('15') + }) + }) + + describe('Form Validation', () => { + it('shows a required field error only after attempting to save without an account name', async () => { + const { user, mockApi } = await renderManualAccountForm() + + expect(screen.queryByText(/is required/i)).not.toBeInTheDocument() + + await user.click(screen.getByTestId('save-manual-account-button')) + + expect((await screen.findAllByText(/is required/i)).length).toBeGreaterThan(0) + expect(mockApi.createAccount).not.toHaveBeenCalled() + }) + }) + + describe('Form Submission', () => { + it('creates the account with the entered values and shows the success view', async () => { + const { user, mockApi } = await renderManualAccountForm() + + await user.type(screen.getByLabelText(/account name/i), 'Test Account') + await user.type(screen.getByLabelText(/account balance/i), '1000') + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + expect(mockApi.createAccount).toHaveBeenCalledWith( + expect.objectContaining({ + user_name: 'Test Account', + balance: '1000', + account_type: AccountTypes.CHECKING, + is_personal: true, + }), + ) + }) + + it('creates a business account when business is selected', async () => { + const { user, mockApi } = await renderManualAccountForm() + + await user.click(screen.getByLabelText('Business')) + await user.type(screen.getByLabelText(/account name/i), 'Business Account') + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + expect(mockApi.createAccount).toHaveBeenCalledWith( + expect.objectContaining({ is_personal: false }), + ) + }) + + it('creates a savings account for the savings account type', async () => { + const { user, mockApi } = await renderManualAccountForm({ accountType: AccountTypes.SAVINGS }) + + await user.type(screen.getByLabelText(/account name/i), 'Savings Account') + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + expect(mockApi.createAccount).toHaveBeenCalledWith( + expect.objectContaining({ account_type: AccountTypes.SAVINGS }), + ) + }) + + it('creates the account when a manual member already exists', async () => { + const { user } = await renderManualAccountForm({ + preloadedMembers: [{ institution_guid: 'INS-MANUAL-456' }], + }) + + await user.type(screen.getByLabelText(/account name/i), 'My Account') + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + }) + }) + + describe('Error Handling', () => { + it('shows an error and re-enables the save button when account creation fails', async () => { + const { user } = await renderManualAccountForm({ + apiOverrides: { + createAccount: vi.fn().mockRejectedValue(new Error('Network error')), + }, + }) + + await user.type(screen.getByLabelText(/account name/i), 'My Account') + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByText('Something went wrong')).toBeInTheDocument() + expect(screen.getByTestId('something-went-wrong-text')).toHaveTextContent( + 'Please try saving your account again.', + ) + expect(screen.getByTestId('save-manual-account-button')).not.toBeDisabled() + }) + + it('allows retrying after an error and reaches the success view', async () => { + const { user } = await renderManualAccountForm({ + apiOverrides: { + createAccount: vi + .fn() + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce({ + member_guid: member.member.guid, + institution_guid: institutionData.institution.guid, + }), + }, + }) + + await user.type(screen.getByLabelText(/account name/i), 'My Account') + await user.click(screen.getByTestId('save-manual-account-button')) + + await screen.findByText('Something went wrong') + + await user.click(screen.getByTestId('save-manual-account-button')) + + expect(await screen.findByTestId('manual-account-success-header')).toBeInTheDocument() + }) + }) +})