diff --git a/src/views/microdeposits/RoutingNumber.js b/src/views/microdeposits/RoutingNumber.js
index 30ae9909a0..81365b0ab0 100644
--- a/src/views/microdeposits/RoutingNumber.js
+++ b/src/views/microdeposits/RoutingNumber.js
@@ -86,7 +86,7 @@ export const RoutingNumber = (props) => {
})
// If reason is IAV_PREFERRED, load institutions to prepare for user choice.
- if (resp.blocked_routing_number.reason === BLOCKED_REASONS.IAV_PREFERRED) {
+ if (resp.blocked_routing_number.reason_name === BLOCKED_REASONS.IAV_PREFERRED) {
const loadedInstitutions$ = defer(() =>
api.loadInstitutions({
routing_number: values.routingNumber,
diff --git a/src/views/microdeposits/__tests__/RoutingNumber-test.js b/src/views/microdeposits/__tests__/RoutingNumber-test.js
new file mode 100644
index 0000000000..d9f44cff44
--- /dev/null
+++ b/src/views/microdeposits/__tests__/RoutingNumber-test.js
@@ -0,0 +1,763 @@
+import React from 'react'
+import { screen, render, waitFor } from 'src/utilities/testingLibrary'
+import { RoutingNumber } from 'src/views/microdeposits/RoutingNumber'
+import { initialState } from 'src/services/mockedData'
+import { BLOCKED_REASONS } from 'src/views/microdeposits/const'
+import { ApiProvider } from 'src/context/ApiContext'
+import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
+import { PostMessageContext } from 'src/ConnectWidget'
+
+vi.mock('src/utilities/Animation', () => ({
+ fadeOut: vi.fn(() => Promise.resolve()),
+}))
+
+describe('RoutingNumber', () => {
+ let props
+ let onPostMessage
+
+ beforeEach(() => {
+ onPostMessage = vi.fn()
+ props = {
+ accountDetails: {},
+ onContinue: vi.fn(),
+ setShowSharedRoutingNumber: vi.fn(),
+ stepToIAV: vi.fn(),
+ }
+ })
+
+ describe('Initial Rendering', () => {
+ it('renders the routing number form with correct header', async () => {
+ render()
+
+ expect(await screen.findByText('Enter routing number')).toBeInTheDocument()
+ expect(screen.getByLabelText('Routing number *')).toBeInTheDocument()
+ expect(
+ screen.getByRole('button', { name: 'Continue to confirm details' }),
+ ).toBeInTheDocument()
+ })
+
+ it('auto-focuses the routing number input field', async () => {
+ render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await waitFor(() => {
+ expect(input).toHaveFocus()
+ })
+ })
+
+ it('shows help finding routing number link', async () => {
+ render()
+
+ expect(await screen.findByText('Help finding your routing number')).toBeInTheDocument()
+ })
+
+ it('shows required field note', async () => {
+ render()
+
+ expect(await screen.findByText('Required')).toBeInTheDocument()
+ })
+
+ it('pre-populates routing number from accountDetails', async () => {
+ const propsWithAccountDetails = {
+ ...props,
+ accountDetails: { routing_number: '123456789' },
+ }
+ render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ expect(input.value).toBe('123456789')
+ })
+ })
+
+ describe('Form Validation', () => {
+ it('shows error when routing number is empty and form is submitted', async () => {
+ const { user } = render()
+
+ const submitButton = await screen.findByRole('button', {
+ name: 'Continue to confirm details',
+ })
+ await user.click(submitButton)
+
+ const input = screen.getByTestId('routing-number-input')
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(screen.getAllByText('Routing number is required')[0]).toBeInTheDocument()
+ })
+
+ it('shows error when routing number is not 9 characters', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '12345')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(screen.getAllByText('Routing number must be 9 characters')[0]).toBeInTheDocument()
+ })
+
+ it('shows error when routing number contains non-digit characters', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '12345abc9')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(screen.getAllByText('Routing number must only contain digits')[0]).toBeInTheDocument()
+ })
+
+ it('focuses on routing number input when validation error occurs', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '12345')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveFocus()
+ })
+ })
+ })
+
+ describe('Success Cases - Valid Routing Number (Not Blocked)', () => {
+ it('calls onContinue with account details when routing number is valid and not blocked', async () => {
+ const verifyRoutingNumber = vi.fn().mockResolvedValue({})
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalledWith('123456789', false)
+ expect(props.onContinue).toHaveBeenCalledWith({
+ routing_number: '123456789',
+ })
+ })
+ })
+
+ it('calls onContinue with merged account details when existing details are present', async () => {
+ const verifyRoutingNumber = vi.fn().mockResolvedValue({})
+ const propsWithDetails = {
+ ...props,
+ accountDetails: {
+ account_number: '987654321',
+ account_type: 1,
+ },
+ }
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(props.onContinue).toHaveBeenCalledWith({
+ account_number: '987654321',
+ account_type: 1,
+ routing_number: '123456789',
+ })
+ })
+ })
+
+ it('passes includeIdentity flag to verifyRoutingNumber when enabled', async () => {
+ const verifyRoutingNumber = vi.fn().mockResolvedValue({})
+ const { user } = render(
+
+
+ ,
+ {
+ preloadedState: {
+ ...initialState,
+ config: {
+ ...initialState.config,
+ include_identity: true,
+ },
+ },
+ },
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalledWith('123456789', true)
+ })
+ })
+
+ it('shows "Verifying..." text on button while submitting', async () => {
+ let resolveVerify
+ const verifyRoutingNumber = vi.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveVerify = resolve
+ }),
+ )
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Checking...')).toBeInTheDocument()
+ })
+
+ resolveVerify({})
+ })
+
+ it('disables input and button while submitting', async () => {
+ let resolveVerify
+ const verifyRoutingNumber = vi.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveVerify = resolve
+ }),
+ )
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toBeDisabled()
+ expect(submitButton).toBeDisabled()
+ })
+
+ resolveVerify({})
+ })
+ })
+
+ describe('Blocked Routing Number Cases', () => {
+ it('shows error message when routing number is blocked', async () => {
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.BLOCKED,
+ reason_name: 'BLOCKED',
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const { user } = render(
+
+
+ ,
+ {
+ preloadedState: {
+ ...initialState,
+ config: {
+ ...initialState.config,
+ },
+ },
+ },
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ const errorMessages = screen.getAllByText(
+ 'Institution is not supported for microdeposit verification.',
+ )
+ expect(errorMessages.length).toBeGreaterThan(0)
+ expect(errorMessages[0]).toBeInTheDocument()
+ })
+
+ it('sends post message when routing number is blocked', async () => {
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.CLIENT_BLOCKED,
+ reason_name: 'CLIENT_BLOCKED',
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const { user } = render(
+
+
+
+
+ ,
+ {
+ preloadedState: {
+ ...initialState,
+ config: {
+ ...initialState.config,
+ },
+ },
+ },
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ const errorMessages = screen.getAllByText(
+ 'Institution is not supported for microdeposit verification.',
+ )
+ expect(errorMessages.length).toBeGreaterThan(0)
+ expect(errorMessages[0]).toBeInTheDocument()
+
+ expect(onPostMessage).toHaveBeenCalledWith('connect/microdeposits/blockedRoutingNumber', {
+ routing_number: '123456789',
+ reason: 'CLIENT_BLOCKED',
+ })
+ })
+
+ it('continues with microdeposit flow when IAV_PREFERRED but no institutions found', async () => {
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.IAV_PREFERRED,
+ reason_name: BLOCKED_REASONS.IAV_PREFERRED,
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const loadInstitutions = vi.fn().mockResolvedValue([])
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalledWith('123456789', false)
+ })
+
+ await waitFor(() => {
+ expect(loadInstitutions).toHaveBeenCalledWith({
+ routing_number: '123456789',
+ account_verification_is_enabled: true,
+ account_identification_is_enabled: false,
+ })
+ })
+
+ await waitFor(() => {
+ expect(props.onContinue).toHaveBeenCalledWith({
+ routing_number: '123456789',
+ })
+ })
+ })
+
+ it('shows SharedRoutingNumber when IAV_PREFERRED and institutions are found', async () => {
+ const institutions = [
+ {
+ guid: 'INS-123',
+ name: 'Test Bank 1',
+ code: 'test_bank_1',
+ url: 'https://testbank1.com',
+ },
+ {
+ guid: 'INS-456',
+ name: 'Test Bank 2',
+ code: 'test_bank_2',
+ url: 'https://testbank2.com',
+ },
+ ]
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.IAV_PREFERRED,
+ reason_name: BLOCKED_REASONS.IAV_PREFERRED,
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const loadInstitutions = vi.fn().mockResolvedValue(institutions)
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalledWith('123456789', false)
+ })
+
+ await waitFor(() => {
+ expect(loadInstitutions).toHaveBeenCalled()
+ })
+
+ expect(await screen.findByText('Select how to connect your account')).toBeInTheDocument()
+ })
+
+ it('calls loadInstitutions with include_identity flag when enabled', async () => {
+ const institutions = [
+ {
+ guid: 'INS-123',
+ name: 'Test Bank 1',
+ code: 'test_bank_1',
+ },
+ ]
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.IAV_PREFERRED,
+ reason_name: BLOCKED_REASONS.IAV_PREFERRED,
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const loadInstitutions = vi.fn().mockResolvedValue(institutions)
+
+ const { user } = render(
+
+
+ ,
+ {
+ preloadedState: {
+ ...initialState,
+ config: {
+ ...initialState.config,
+ include_identity: true,
+ },
+ },
+ },
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalledWith('123456789', true)
+ })
+
+ await waitFor(() => {
+ expect(loadInstitutions).toHaveBeenCalledWith({
+ routing_number: '123456789',
+ account_verification_is_enabled: true,
+ account_identification_is_enabled: true,
+ })
+ })
+ })
+ })
+
+ describe('API Error Handling', () => {
+ it('shows error message when verifyRoutingNumber API call fails', async () => {
+ const verifyRoutingNumber = vi.fn().mockRejectedValue({
+ response: { status: 500 },
+ })
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(
+ screen.getAllByText('Unable to proceed. Please try again later. Error: 500')[0],
+ ).toBeInTheDocument()
+ })
+
+ it('shows error with UNKNOWN status when error response has no status', async () => {
+ const verifyRoutingNumber = vi.fn().mockRejectedValue({})
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(
+ screen.getAllByText('Unable to proceed. Please try again later. Error: UNKNOWN')[0],
+ ).toBeInTheDocument()
+ })
+
+ it('re-enables form after API error', async () => {
+ const verifyRoutingNumber = vi.fn().mockRejectedValue({
+ response: { status: 500 },
+ })
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).not.toBeDisabled()
+ expect(submitButton).not.toBeDisabled()
+ })
+ })
+ })
+
+ describe('Navigation', () => {
+ it('shows FindAccountInfo when help link is clicked', async () => {
+ const { user } = render()
+
+ const helpLink = await screen.findByText('Help finding your routing number')
+ await user.click(helpLink)
+
+ expect(await screen.findByText('Find your routing number')).toBeInTheDocument()
+ expect(screen.getByText('Mobile app or online portal')).toBeInTheDocument()
+ })
+
+ it('returns to routing number form when FindAccountInfo is closed', async () => {
+ const { user } = render()
+
+ const helpLink = await screen.findByText('Help finding your routing number')
+ await user.click(helpLink)
+
+ const closeButton = await screen.findByRole('button', { name: 'Continue' })
+ await user.click(closeButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Enter routing number')).toBeInTheDocument()
+ })
+ })
+
+ it('renders SharedRoutingNumber component when institutions are found', async () => {
+ const institutions = [
+ {
+ guid: 'INS-123',
+ name: 'Test Bank 1',
+ code: 'test_bank_1',
+ },
+ ]
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.IAV_PREFERRED,
+ reason_name: BLOCKED_REASONS.IAV_PREFERRED,
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const loadInstitutions = vi.fn().mockResolvedValue(institutions)
+
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Select how to connect your account')).toBeInTheDocument()
+ })
+
+ expect(screen.getByText('Instant')).toBeInTheDocument()
+ expect(screen.getByText('2-3 days')).toBeInTheDocument()
+ expect(screen.getByText('Test Bank 1')).toBeInTheDocument()
+ })
+ })
+
+ describe('Accessibility', () => {
+ it('associates error message with input using aria-describedby', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '12345')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-describedby', 'routingNumber-error')
+ })
+ })
+
+ it('announces validation errors to screen readers via AriaLive', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '12345')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ const ariaLiveRegion = document.querySelector('[aria-live="assertive"]')
+ await waitFor(() => {
+ expect(ariaLiveRegion).toHaveTextContent('Routing number must be 9 characters')
+ })
+ })
+
+ it('announces routing blocked errors to screen readers via AriaLive', async () => {
+ const blockedResponse = {
+ blocked_routing_number: {
+ guid: 'BLK-123',
+ reason: BLOCKED_REASONS.BLOCKED,
+ reason_name: 'BLOCKED',
+ },
+ }
+ const verifyRoutingNumber = vi.fn().mockResolvedValue(blockedResponse)
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ const ariaLiveRegions = document.querySelectorAll('[aria-live="assertive"]')
+ await waitFor(() => {
+ const hasMessage = Array.from(ariaLiveRegions).some((region) =>
+ region.textContent.includes(
+ 'Institution is not supported for microdeposit verification.',
+ ),
+ )
+ expect(hasMessage).toBe(true)
+ })
+ })
+ })
+
+ describe('Form Interaction', () => {
+ it('allows user to change routing number value', async () => {
+ const { user } = render()
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+ expect(input.value).toBe('123456789')
+
+ await user.clear(input)
+ await user.type(input, '987654321')
+ expect(input.value).toBe('987654321')
+ })
+
+ it('handles form submission correctly', async () => {
+ const verifyRoutingNumber = vi.fn().mockResolvedValue({})
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ await user.type(input, '123456789')
+
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(verifyRoutingNumber).toHaveBeenCalled()
+ expect(props.onContinue).toHaveBeenCalled()
+ })
+ })
+
+ it('clears validation errors when valid input is entered and resubmitted', async () => {
+ const verifyRoutingNumber = vi.fn().mockResolvedValue({})
+ const { user } = render(
+
+
+ ,
+ )
+
+ const input = await screen.findByTestId('routing-number-input')
+ const submitButton = screen.getByRole('button', { name: 'Continue to confirm details' })
+
+ // Submit with invalid input
+ await user.type(input, '12345')
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).toHaveAttribute('aria-invalid', 'true')
+ })
+ expect(screen.getAllByText('Routing number must be 9 characters')[0]).toBeInTheDocument()
+
+ // Fix input and resubmit
+ await user.clear(input)
+ await user.type(input, '123456789')
+ await user.click(submitButton)
+
+ await waitFor(() => {
+ expect(input).not.toHaveAttribute('aria-invalid', 'true')
+ })
+ })
+ })
+})