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() + }) + }) +})