diff --git a/src/views/oauth/OAuthStartError-test.tsx b/src/views/oauth/OAuthStartError-test.tsx
new file mode 100644
index 0000000000..0d47a0c1fa
--- /dev/null
+++ b/src/views/oauth/OAuthStartError-test.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import { describe, expect, it, vi } from 'vitest'
+
+import { OAuthStartError } from 'src/views/oauth/OAuthStartError'
+import { OAuthStep } from 'src/views/oauth/OAuthStep'
+import { render, screen } from 'src/utilities/testingLibrary'
+import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
+import { institutionData, member } from 'src/services/mockedData'
+
+describe('OAuthStartError', () => {
+ const defaultProps = {
+ institution: institutionData.institution,
+ oauthStartError: { response: { status: 500 } },
+ onOAuthTryAgain: () => {},
+ }
+
+ describe('Rendering', () => {
+ it('renders the institution, the error title and message, and a Try again button', () => {
+ render()
+
+ expect(screen.getByText(institutionData.institution.name)).toBeInTheDocument()
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument()
+ expect(screen.getByText('Please try again or come back later.')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument()
+ })
+
+ it('shows the 403 error message when verification is not enabled', () => {
+ render()
+
+ expect(
+ screen.getByText('Verification must be enabled to use this feature.'),
+ ).toBeInTheDocument()
+ })
+
+ it('shows the 409 error message for a conflicting request', () => {
+ render()
+
+ expect(
+ screen.getByText(
+ 'Oops! There was a problem. Please check your username and password, and try again.',
+ ),
+ ).toBeInTheDocument()
+ })
+ })
+
+ describe('Try again', () => {
+ it('restarts the OAuth flow and shows the sign-in view when clicked', async () => {
+ const OAuthStepComponent = OAuthStep as unknown as React.ComponentType<{
+ institution: typeof institutionData.institution
+ onGoBack: () => void
+ }>
+ const addMember = vi
+ .fn()
+ .mockRejectedValueOnce({ response: { status: 500 } })
+ .mockResolvedValue({ member: member.member })
+
+ const { user } = render(
+ {}} />,
+ {
+ apiValue: { ...apiValueMock, addMember } as unknown as typeof apiValueMock,
+ },
+ )
+
+ expect(await screen.findByText('Something went wrong')).toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: 'Try again' }))
+ expect(
+ await screen.findByText(`Log in at ${institutionData.institution.name}`),
+ ).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/views/search/views/PopularInstitutionsList-test.tsx b/src/views/search/views/PopularInstitutionsList-test.tsx
new file mode 100644
index 0000000000..a6072e01b9
--- /dev/null
+++ b/src/views/search/views/PopularInstitutionsList-test.tsx
@@ -0,0 +1,120 @@
+import React from 'react'
+import { describe, expect, it } from 'vitest'
+
+import RenderConnectStep from 'src/components/RenderConnectStep'
+import { render, screen } from 'src/utilities/testingLibrary'
+import { initialState } from 'src/services/mockedData'
+import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
+import { AccountTypes } from 'src/views/manualAccount/constants'
+import { STEPS } from 'src/const/Connect'
+
+type StateOverrides = {
+ enableManualAccounts?: boolean
+ mode?: string
+ hasAtriumApi?: boolean
+}
+
+const buildSearchState = ({
+ enableManualAccounts = true,
+ mode = 'aggregation',
+ hasAtriumApi = false,
+}: StateOverrides = {}) => ({
+ ...initialState,
+ config: { ...initialState.config, mode },
+ connect: {
+ ...initialState.connect,
+ location: [{ step: STEPS.SEARCH }],
+ },
+ profiles: {
+ ...initialState.profiles,
+ client: { ...initialState.profiles.client, has_atrium_api: hasAtriumApi },
+ widgetProfile: {
+ ...initialState.profiles.widgetProfile,
+ enable_manual_accounts: enableManualAccounts,
+ },
+ },
+})
+
+const renderSearchStep = (preloadedState = buildSearchState()) =>
+ render(
+ {}}
+ handleCredentialsGoBack={() => {}}
+ navigationRef={React.createRef()}
+ onManualAccountAdded={() => {}}
+ onUpsertMember={() => {}}
+ setConnectLocalState={() => {}}
+ />,
+ { apiValue: apiValueMock, preloadedState },
+ )
+
+describe('PopularInstitutionsList', () => {
+ describe('Rendering', () => {
+ it('renders the enabled popular institutions, the search button, and hides client-disabled ones', async () => {
+ renderSearchStep()
+
+ expect(await screen.findByText('Gringotts')).toBeInTheDocument()
+ expect(screen.getByText('American Express Credit Card')).toBeInTheDocument()
+ expect(screen.getByText('Discover Credit Card')).toBeInTheDocument()
+ expect(screen.getByText('Capital One')).toBeInTheDocument()
+ expect(
+ screen.getByRole('button', { name: 'Search for your institution' }),
+ ).toBeInTheDocument()
+ // Chase is disabled_by_client and should be filtered out.
+ expect(screen.queryByText('Chase')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Search button', () => {
+ it('focuses the search input when the search button is clicked', async () => {
+ const { user } = renderSearchStep()
+
+ await user.click(await screen.findByRole('button', { name: 'Search for your institution' }))
+
+ expect(screen.getByTestId('search-input')).toHaveFocus()
+ })
+ })
+
+ describe('Institution selection', () => {
+ it('advances past the search screen when an institution is selected', async () => {
+ const { user } = renderSearchStep()
+
+ await user.click(await screen.findByRole('button', { name: 'Add account with Gringotts' }))
+
+ expect(await screen.findByText('Log in at Test Bank')).toBeInTheDocument()
+ expect(screen.queryByTestId('search-header')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Manual accounts', () => {
+ it('shows the add-account-manually button and navigates to the menu when clicked', async () => {
+ const { user } = renderSearchStep(buildSearchState({ enableManualAccounts: true }))
+
+ await user.click(await screen.findByRole('button', { name: 'Add account manually' }))
+
+ expect(await screen.findByTestId('manual-account-menu-container')).toBeInTheDocument()
+ })
+
+ it('hides the button when manual accounts are disabled', async () => {
+ renderSearchStep(buildSearchState({ enableManualAccounts: false }))
+
+ await screen.findByRole('button', { name: 'Search for your institution' })
+ expect(screen.queryByRole('button', { name: 'Add account manually' })).not.toBeInTheDocument()
+ })
+
+ it('hides the button in verification mode', async () => {
+ renderSearchStep(buildSearchState({ mode: 'verification' }))
+
+ await screen.findByRole('button', { name: 'Search for your institution' })
+ expect(screen.queryByRole('button', { name: 'Add account manually' })).not.toBeInTheDocument()
+ })
+
+ it('hides the button when the client uses the Atrium API', async () => {
+ renderSearchStep(buildSearchState({ hasAtriumApi: true }))
+
+ await screen.findByRole('button', { name: 'Search for your institution' })
+ expect(screen.queryByRole('button', { name: 'Add account manually' })).not.toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/views/search/views/SearchFailed-test.tsx b/src/views/search/views/SearchFailed-test.tsx
new file mode 100644
index 0000000000..3c86fb3f67
--- /dev/null
+++ b/src/views/search/views/SearchFailed-test.tsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import { describe, expect, it } from 'vitest'
+
+import { SearchFailed } from 'src/views/search/views/SearchFailed'
+import { render, screen } from 'src/utilities/testingLibrary'
+
+describe('SearchFailed', () => {
+ it('renders the search error title and message', () => {
+ render()
+
+ expect(screen.getByText(/Search isn.t working/)).toBeVisible()
+ expect(screen.getByText('Something went wrong. Please try again.')).toBeVisible()
+ })
+})
diff --git a/src/views/search/views/SearchNoResult-test.tsx b/src/views/search/views/SearchNoResult-test.tsx
new file mode 100644
index 0000000000..5fad223ded
--- /dev/null
+++ b/src/views/search/views/SearchNoResult-test.tsx
@@ -0,0 +1,145 @@
+import React from 'react'
+import { describe, expect, it } from 'vitest'
+
+import RenderConnectStep from 'src/components/RenderConnectStep'
+import { fireEvent, render, screen } from 'src/utilities/testingLibrary'
+import { initialState } from 'src/services/mockedData'
+import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
+import { AccountTypes } from 'src/views/manualAccount/constants'
+import { STEPS } from 'src/const/Connect'
+
+type StateOverrides = {
+ mode?: string
+ enableManualAccounts?: boolean
+ hasAtriumApi?: boolean
+ accountVerificationEnabled?: boolean
+ isMicrodepositsEnabled?: boolean
+ showMicrodeposits?: boolean
+}
+
+const buildSearchState = ({
+ mode = 'aggregation',
+ enableManualAccounts = true,
+ hasAtriumApi = false,
+ accountVerificationEnabled = false,
+ isMicrodepositsEnabled = false,
+ showMicrodeposits = false,
+}: StateOverrides = {}) => ({
+ ...initialState,
+ config: { ...initialState.config, mode },
+ connect: {
+ ...initialState.connect,
+ location: [{ step: STEPS.SEARCH }],
+ },
+ profiles: {
+ ...initialState.profiles,
+ client: { ...initialState.profiles.client, has_atrium_api: hasAtriumApi },
+ clientProfile: {
+ ...initialState.profiles.clientProfile,
+ account_verification_is_enabled: accountVerificationEnabled,
+ is_microdeposits_enabled: isMicrodepositsEnabled,
+ },
+ widgetProfile: {
+ ...initialState.profiles.widgetProfile,
+ enable_manual_accounts: enableManualAccounts,
+ show_microdeposits_in_connect: showMicrodeposits,
+ },
+ },
+})
+
+const microdepositsEnabledState = () =>
+ buildSearchState({
+ mode: 'verification',
+ accountVerificationEnabled: true,
+ isMicrodepositsEnabled: true,
+ showMicrodeposits: true,
+ })
+
+const renderNoResultStep = (preloadedState = buildSearchState()) =>
+ render(
+ {}}
+ handleCredentialsGoBack={() => {}}
+ navigationRef={React.createRef()}
+ onManualAccountAdded={() => {}}
+ onUpsertMember={() => {}}
+ setConnectLocalState={() => {}}
+ />,
+ {
+ apiValue: { ...apiValueMock, loadInstitutions: () => Promise.resolve([]) },
+ preloadedState,
+ },
+ )
+
+const startSearch = async (term = 'test bank') => {
+ fireEvent.change(await screen.findByPlaceholderText('Search'), { target: { value: term } })
+}
+
+describe('SearchNoResult', () => {
+ describe('Rendering', () => {
+ it('renders the no-results message with the search term and a suggestion', async () => {
+ renderNoResultStep()
+
+ await startSearch('test bank')
+
+ const message = await screen.findByTestId('0-search-results')
+ expect(message).toHaveTextContent(/No results found/)
+ expect(message).toHaveTextContent(/test bank/)
+ expect(
+ screen.getByText('Check spelling and try again, or try searching for another institution.'),
+ ).toBeVisible()
+ })
+ })
+
+ describe('Manual accounts', () => {
+ it('shows the add-account-manually button and navigates to the menu when clicked', async () => {
+ const { user } = renderNoResultStep()
+
+ await startSearch()
+
+ await user.click(await screen.findByRole('button', { name: 'Add account manually' }))
+
+ expect(await screen.findByTestId('manual-account-menu-container')).toBeInTheDocument()
+ })
+
+ it.each([
+ ['manual accounts are disabled', buildSearchState({ enableManualAccounts: false })],
+ ['in verification mode', buildSearchState({ mode: 'verification' })],
+ ['the client uses the Atrium API', buildSearchState({ hasAtriumApi: true })],
+ ])('hides the button when %s', async (_case, state) => {
+ renderNoResultStep(state)
+
+ await startSearch()
+ await screen.findByTestId('0-search-results')
+
+ expect(screen.queryByRole('button', { name: 'Add account manually' })).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Microdeposits', () => {
+ it('shows the connect-with-account-numbers button and navigates to microdeposits', async () => {
+ const { user } = renderNoResultStep(microdepositsEnabledState())
+
+ await startSearch()
+
+ await user.click(await screen.findByRole('button', { name: 'Connect with account numbers' }))
+
+ expect(await screen.findByText('Enter routing number')).toBeInTheDocument()
+ })
+
+ it.each([
+ ['not in verification mode', buildSearchState()],
+ ['verification is not enabled', buildSearchState({ mode: 'verification' })],
+ ])('hides the button when %s', async (_case, state) => {
+ renderNoResultStep(state)
+
+ await startSearch()
+ await screen.findByTestId('0-search-results')
+
+ expect(
+ screen.queryByRole('button', { name: 'Connect with account numbers' }),
+ ).not.toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/views/search/views/SearchedInstitutionsList-test.tsx b/src/views/search/views/SearchedInstitutionsList-test.tsx
new file mode 100644
index 0000000000..d7278360b3
--- /dev/null
+++ b/src/views/search/views/SearchedInstitutionsList-test.tsx
@@ -0,0 +1,180 @@
+import React from 'react'
+import { describe, expect, it } from 'vitest'
+
+import RenderConnectStep from 'src/components/RenderConnectStep'
+import { fireEvent, render, screen } from 'src/utilities/testingLibrary'
+import { initialState, SEARCHED_INSTITUTIONS } from 'src/services/mockedData'
+import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
+import { AccountTypes } from 'src/views/manualAccount/constants'
+import { STEPS } from 'src/const/Connect'
+
+type StateOverrides = {
+ mode?: string
+ enableManualAccounts?: boolean
+ hasAtriumApi?: boolean
+ accountVerificationEnabled?: boolean
+ isMicrodepositsEnabled?: boolean
+ showMicrodeposits?: boolean
+}
+
+const buildSearchState = ({
+ mode = 'aggregation',
+ enableManualAccounts = true,
+ hasAtriumApi = false,
+ accountVerificationEnabled = false,
+ isMicrodepositsEnabled = false,
+ showMicrodeposits = false,
+}: StateOverrides = {}) => ({
+ ...initialState,
+ config: { ...initialState.config, mode },
+ connect: {
+ ...initialState.connect,
+ location: [{ step: STEPS.SEARCH }],
+ },
+ profiles: {
+ ...initialState.profiles,
+ client: { ...initialState.profiles.client, has_atrium_api: hasAtriumApi },
+ clientProfile: {
+ ...initialState.profiles.clientProfile,
+ account_verification_is_enabled: accountVerificationEnabled,
+ is_microdeposits_enabled: isMicrodepositsEnabled,
+ },
+ widgetProfile: {
+ ...initialState.profiles.widgetProfile,
+ enable_manual_accounts: enableManualAccounts,
+ show_microdeposits_in_connect: showMicrodeposits,
+ },
+ },
+})
+
+const microdepositsEnabledState = () =>
+ buildSearchState({
+ mode: 'verification',
+ accountVerificationEnabled: true,
+ isMicrodepositsEnabled: true,
+ showMicrodeposits: true,
+ })
+
+const renderSearchStep = (
+ preloadedState = buildSearchState(),
+ apiOverrides: Partial = {},
+) =>
+ render(
+ {}}
+ handleCredentialsGoBack={() => {}}
+ navigationRef={React.createRef()}
+ onManualAccountAdded={() => {}}
+ onUpsertMember={() => {}}
+ setConnectLocalState={() => {}}
+ />,
+ { apiValue: { ...apiValueMock, ...apiOverrides }, preloadedState },
+ )
+
+const startSearch = async () => {
+ fireEvent.change(await screen.findByPlaceholderText('Search'), { target: { value: 'test' } })
+}
+
+describe('SearchedInstitutionsList', () => {
+ describe('Rendering', () => {
+ it('renders the search results, the result count, and a load-more button', async () => {
+ renderSearchStep()
+
+ await startSearch()
+
+ expect(await screen.findByText('MX Bank')).toBeInTheDocument()
+ expect(screen.getByText('Grinnell State Bank')).toBeInTheDocument()
+ expect(screen.getAllByText('5 search results').length).toBeGreaterThan(0)
+ expect(screen.getByRole('button', { name: 'Load more institutions' })).toBeInTheDocument()
+ })
+
+ it('shows a singular result count when a single institution matches', async () => {
+ renderSearchStep(buildSearchState(), {
+ loadInstitutions: () => Promise.resolve([SEARCHED_INSTITUTIONS[1]]),
+ })
+
+ await startSearch()
+
+ expect(await screen.findByText('MX Bank')).toBeInTheDocument()
+ expect(screen.getAllByText('1 search result').length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Institution selection', () => {
+ it('advances past the search screen when a searched institution is selected', async () => {
+ const { user } = renderSearchStep()
+
+ await startSearch()
+
+ await user.click(await screen.findByRole('button', { name: 'Add account with MX Bank' }))
+
+ expect(await screen.findByText('Log in at Test Bank')).toBeInTheDocument()
+ expect(screen.queryByTestId('search-header')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Load more institutions', () => {
+ it('appends the next page of results when the load-more button is clicked', async () => {
+ const { user } = renderSearchStep()
+
+ await startSearch()
+ await screen.findByText('Grinnell State Bank')
+
+ await user.click(screen.getByRole('button', { name: 'Load more institutions' }))
+
+ expect((await screen.findAllByText('10 search results')).length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Manual accounts', () => {
+ it('shows the add-account-manually button and navigates to the menu when clicked', async () => {
+ const { user } = renderSearchStep()
+
+ await startSearch()
+
+ await user.click(await screen.findByRole('button', { name: 'Add account manually' }))
+
+ expect(await screen.findByTestId('manual-account-menu-container')).toBeInTheDocument()
+ })
+
+ it.each([
+ ['manual accounts are disabled', buildSearchState({ enableManualAccounts: false })],
+ ['in verification mode', buildSearchState({ mode: 'verification' })],
+ ['the client uses the Atrium API', buildSearchState({ hasAtriumApi: true })],
+ ])('hides the button when %s', async (_case, state) => {
+ renderSearchStep(state)
+
+ await startSearch()
+ await screen.findByText('Grinnell State Bank')
+
+ expect(screen.queryByRole('button', { name: 'Add account manually' })).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Microdeposits', () => {
+ it('shows the connect-with-account-numbers button and navigates to microdeposits', async () => {
+ const { user } = renderSearchStep(microdepositsEnabledState())
+
+ await startSearch()
+
+ await user.click(await screen.findByRole('button', { name: 'Connect with account numbers' }))
+
+ expect(await screen.findByText('Enter routing number')).toBeInTheDocument()
+ })
+
+ it.each([
+ ['not in verification mode', buildSearchState()],
+ ['verification is not enabled', buildSearchState({ mode: 'verification' })],
+ ])('hides the button when %s', async (_case, state) => {
+ renderSearchStep(state)
+
+ await startSearch()
+ await screen.findByText('Grinnell State Bank')
+
+ expect(
+ screen.queryByRole('button', { name: 'Connect with account numbers' }),
+ ).not.toBeInTheDocument()
+ })
+ })
+})