import { Result } from '@badrap/result'
import { FiatAccountSchema, FiatAccountType } from '@fiatconnect/fiatconnect-types'
import { fireEvent, render } from '@testing-library/react-native'
import _ from 'lodash'
import * as React from 'react'
import { Provider } from 'react-redux'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { FiatExchangeEvents } from 'src/analytics/Events'
import FiatConnectQuote from 'src/fiatExchanges/quotes/FiatConnectQuote'
import { CICOFlow } from 'src/fiatExchanges/types'
import { FiatConnectQuoteSuccess } from 'src/fiatconnect'
import { SendingFiatAccountStatus, submitFiatAccount } from 'src/fiatconnect/slice'
import { FiatAccountSchemaCountryOverrides } from 'src/fiatconnect/types'
import { navigateBack, navigateHome } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
import {
mockCusdTokenId,
mockFiatConnectProviderIcon,
mockFiatConnectQuotes,
mockNavigation,
} from 'test/values'
import FiatDetailsScreen from './FiatDetailsScreen'
jest.mock('src/alert/actions')
jest.mock('src/analytics/AppAnalytics')
jest.mock('src/utils/Logger', () => ({
__esModule: true,
namedExport: jest.fn(),
default: {
info: jest.fn(),
error: jest.fn(),
},
}))
const fakeInstitutionName = 'CapitalTwo Bank'
const fakeAccountNumber = '1234567'
let mockResult = Result.ok({
fiatAccountId: '1234',
accountName: '7890',
institutionName: fakeInstitutionName,
fiatAccountType: 'BankAccount',
})
jest.mock('@fiatconnect/fiatconnect-sdk', () => ({
...(jest.requireActual('@fiatconnect/fiatconnect-sdk') as any),
FiatConnectClient: jest.fn(() => ({
addFiatAccount: jest.fn(() => mockResult),
})),
}))
jest.mock('src/fiatconnect/clients')
const schemaCountryOverrides: FiatAccountSchemaCountryOverrides = {
NG: {
[FiatAccountSchema.AccountNumber]: {
accountNumber: {
regex: '^[0-9]{10}$',
errorString: 'errorMessageDigitLength',
},
},
},
}
const store = createMockStore({ fiatConnect: { schemaCountryOverrides } })
const quoteWithAllowedValues = new FiatConnectQuote({
quote: mockFiatConnectQuotes[1] as FiatConnectQuoteSuccess,
fiatAccountType: FiatAccountType.BankAccount,
flow: CICOFlow.CashIn,
tokenId: mockCusdTokenId,
})
const mockScreenPropsWithAllowedValues = getMockStackScreenProps(Screens.FiatDetailsScreen, {
flow: CICOFlow.CashIn,
quote: quoteWithAllowedValues,
})
// NOTE: Make a quote with no allowed values since setting a value on picker is hard
const mockFcQuote = _.cloneDeep(mockFiatConnectQuotes[1] as FiatConnectQuoteSuccess)
mockFcQuote.fiatAccount.BankAccount = {
...mockFcQuote.fiatAccount.BankAccount,
fiatAccountSchemas: [
{
fiatAccountSchema: FiatAccountSchema.AccountNumber,
allowedValues: {},
},
],
}
const quote = new FiatConnectQuote({
quote: mockFcQuote,
fiatAccountType: FiatAccountType.BankAccount,
flow: CICOFlow.CashIn,
tokenId: mockCusdTokenId,
})
const mockScreenProps = getMockStackScreenProps(Screens.FiatDetailsScreen, {
flow: CICOFlow.CashIn,
quote,
})
const mmQuote = new FiatConnectQuote({
quote: _.cloneDeep(mockFiatConnectQuotes[4] as FiatConnectQuoteSuccess),
fiatAccountType: FiatAccountType.MobileMoney,
flow: CICOFlow.CashIn,
tokenId: mockCusdTokenId,
})
describe('FiatDetailsScreen', () => {
beforeEach(() => {
mockResult = Result.ok({
fiatAccountId: '1234',
accountName: '7890',
institutionName: fakeInstitutionName,
fiatAccountType: FiatAccountType.BankAccount,
})
store.dispatch = jest.fn()
})
afterEach(() => {
jest.clearAllMocks()
})
it('can view a list of bank fields', () => {
const { getByText, getByTestId, queryByTestId } = render(
)
expect(getByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(getByTestId('picker-institutionName')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(getByTestId('input-accountNumber')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
expect(getByTestId('submitButton')).toBeTruthy()
expect(getByTestId('submitButton')).toBeDisabled()
expect(getByText('fiatDetailsScreen.submitAndContinue')).toBeTruthy()
})
it.each`
accountType | accountQuote | translationKey
${'Bank Account'} | ${quote} | ${'fiatDetailsScreen.headerBankAccount'}
${'Mobile Money'} | ${mmQuote} | ${'fiatDetailsScreen.headerMobileMoney'}
`('renders $accountType header with provider image', ({ accountQuote, translationKey }) => {
let headerTitle: React.ReactNode
;(mockNavigation.setOptions as jest.Mock).mockImplementation((options) => {
headerTitle = options.headerTitle()
})
const mockScreenProps = getMockStackScreenProps(Screens.FiatDetailsScreen, {
flow: CICOFlow.CashIn,
quote: accountQuote,
})
render(
)
const { getByText, getByTestId } = render({headerTitle})
expect(getByText(translationKey)).toBeTruthy()
expect(
getByText(
`fiatDetailsScreen.headerSubTitle, {"provider":"${accountQuote.getProviderName()}"}`
)
).toBeTruthy()
expect(getByTestId('headerProviderIcon')).toBeTruthy()
expect(getByTestId('headerProviderIcon').props.source.uri).toEqual(mockFiatConnectProviderIcon)
})
it('cancel button navigates to fiat exchange screen and fires analytics event', () => {
let headerRight: React.ReactNode
;(mockNavigation.setOptions as jest.Mock).mockImplementation((options) => {
headerRight = options.headerRight()
})
render(
)
const { getByText } = render({headerRight})
fireEvent.press(getByText('cancel'))
expect(navigateHome).toHaveBeenCalled()
expect(AppAnalytics.track).toHaveBeenCalledWith(FiatExchangeEvents.cico_fiat_details_cancel, {
flow: mockScreenPropsWithAllowedValues.route.params.flow,
provider: quoteWithAllowedValues.getProviderId(),
fiatAccountSchema: quoteWithAllowedValues.getFiatAccountSchema(),
})
})
it('back button navigates back and fires analytics event', () => {
let headerLeft: React.ReactNode
;(mockNavigation.setOptions as jest.Mock).mockImplementation((options) => {
headerLeft = options.headerLeft()
})
render(
)
const { getByTestId } = render({headerLeft})
expect(getByTestId('backButton')).toBeTruthy()
fireEvent.press(getByTestId('backButton'))
expect(navigateBack).toHaveBeenCalledWith()
expect(AppAnalytics.track).toHaveBeenCalledWith(FiatExchangeEvents.cico_fiat_details_back, {
flow: mockScreenPropsWithAllowedValues.route.params.flow,
provider: quoteWithAllowedValues.getProviderId(),
fiatAccountSchema: quoteWithAllowedValues.getFiatAccountSchema(),
})
})
it('button remains disabled if required input field is empty', () => {
const { getByText, getByTestId, queryByTestId } = render(
)
expect(getByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
fireEvent.changeText(getByTestId('input-accountNumber'), fakeAccountNumber)
expect(getByTestId('submitButton')).toBeDisabled()
})
it('shows validation error if the input field does not fulfill the requirement after delay', () => {
const { getByText, getByTestId, queryByTestId } = render(
)
expect(getByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
fireEvent.changeText(getByTestId('input-accountNumber'), '12dtfa')
// Should see an error message saying the account number field is invalid
// after delay
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
jest.advanceTimersByTime(1500)
expect(getByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.errorMessageDigit')).toBeTruthy()
expect(getByTestId('submitButton')).toBeDisabled()
})
it('shows validation error if the input field does not fulfill the requirement immediately on blur', () => {
const { getByText, getByTestId, queryByTestId } = render(
)
expect(getByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
fireEvent.changeText(getByTestId('input-accountNumber'), '12dtfa')
fireEvent(getByTestId('input-accountNumber'), 'blur')
// Should see an error message saying the account number field is invalid
// immediately since the field loses focus
expect(getByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.errorMessageDigit')).toBeTruthy()
expect(getByTestId('submitButton')).toBeDisabled()
})
it('shows country specific validation error using overrides', () => {
const mockStore = createMockStore({
fiatConnect: { schemaCountryOverrides },
networkInfo: { userLocationData: { countryCodeAlpha2: 'NG' } },
})
const { getByText, getByTestId, queryByTestId } = render(
)
expect(getByText('fiatAccountSchema.institutionName.label')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.label')).toBeTruthy()
expect(queryByTestId(/errorMessage-.+/)).toBeFalsy()
fireEvent.changeText(getByTestId('input-accountNumber'), '123456')
fireEvent(getByTestId('input-accountNumber'), 'blur')
// Should see an error message saying the account number field is invalid
// immediately since the field loses focus
expect(getByTestId('errorMessage-accountNumber')).toBeTruthy()
expect(getByText('fiatAccountSchema.accountNumber.errorMessageDigitLength')).toBeTruthy()
expect(getByTestId('submitButton')).toBeDisabled()
})
it('dispatches to saga when validation passes after pressing submit', async () => {
const { getByTestId } = render(
)
fireEvent.changeText(getByTestId('input-institutionName'), fakeInstitutionName)
fireEvent.changeText(getByTestId('input-accountNumber'), fakeAccountNumber)
const mockFiatAccountData = {
accountName: 'CapitalTwo Bank (...4567)',
institutionName: fakeInstitutionName,
accountNumber: fakeAccountNumber,
country: 'US',
fiatAccountType: 'BankAccount',
}
await fireEvent.press(getByTestId('submitButton'))
expect(store.dispatch).toHaveBeenCalledWith(
submitFiatAccount({
flow: CICOFlow.CashIn,
quote,
fiatAccountData: mockFiatAccountData,
})
)
})
it('shows spinner while fiat account is sending', () => {
const mockStore = createMockStore({
fiatConnect: {
sendingFiatAccountStatus: SendingFiatAccountStatus.Sending,
schemaCountryOverrides,
},
})
const { getByTestId } = render(
)
expect(getByTestId('spinner')).toBeTruthy()
})
it('shows checkmark if fiat account and KYC have been approved', () => {
const mockStore = createMockStore({
fiatConnect: {
sendingFiatAccountStatus: SendingFiatAccountStatus.KycApproved,
schemaCountryOverrides,
},
})
const { getByTestId } = render(
)
expect(getByTestId('checkmark')).toBeTruthy()
})
it('displays info dialog for fields with infoDialog param set', () => {
const mmScreenProps = getMockStackScreenProps(Screens.FiatDetailsScreen, {
flow: CICOFlow.CashIn,
quote: mmQuote,
})
const { getByTestId, getByText, queryByTestId } = render(
)
expect(queryByTestId('dialog-mobile')).toBeFalsy()
fireEvent.press(getByTestId('infoIcon-mobile'))
expect(getByTestId('dialog-mobile')).toBeVisible()
expect(getByText('fiatAccountSchema.mobileMoney.mobileDialog.title')).toBeTruthy()
expect(getByText('fiatAccountSchema.mobileMoney.mobileDialog.dismiss')).toBeTruthy()
expect(getByText('fiatAccountSchema.mobileMoney.mobileDialog.body')).toBeTruthy()
})
})