import { fireEvent, render } from '@testing-library/react-native'
import * as React from 'react'
import { Provider } from 'react-redux'
import { hideAlert } from 'src/alert/actions'
import { HomeEvents } from 'src/analytics/Events'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { toggleHideBalances } from 'src/app/actions'
import { AssetsTokenBalance, FiatExchangeTokenBalance } from 'src/components/TokenBalance'
import { LocalCurrencyCode } from 'src/localCurrency/consts'
import { navigateClearingStack } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getFeatureGate, getMultichainFeatures } from 'src/statsig'
import { NetworkId } from 'src/transactions/types'
import { ONE_DAY_IN_MILLIS } from 'src/utils/time'
import { createMockStore, getElementText } from 'test/utils'
import {
mockCeurTokenId,
mockEthTokenId,
mockPoofTokenId,
mockPositions,
mockTokenBalances,
} from 'test/values'
jest.mock('src/statsig')
jest.mock('src/web3/networkConfig', () => {
const originalModule = jest.requireActual('src/web3/networkConfig')
return {
__esModule: true,
...originalModule,
default: {
...originalModule.default,
networkToNetworkId: {
celo: 'celo-alfajores',
ethereum: 'ethereuim-sepolia',
},
defaultNetworkId: 'celo-alfajores',
},
}
})
const defaultStore = {
tokens: {
tokenBalances: mockTokenBalances, // only one token has non-zero balance, total value is $0.50
},
positions: {
positions: mockPositions, // Total value of positions is ~$7.91
},
localCurrency: {
preferredCurrencyCode: LocalCurrencyCode.USD,
fetchedCurrencyCode: LocalCurrencyCode.USD,
usdToLocalRate: '1',
},
}
const noPositions = {
positions: {
positions: [],
},
}
const noTokens = {
tokens: {
tokenBalances: {},
},
}
const multipleTokens = {
tokens: {
tokenBalances: {
[mockCeurTokenId]: {
...mockTokenBalances[mockCeurTokenId],
balance: '7',
},
[mockEthTokenId]: {
...mockTokenBalances[mockEthTokenId],
balance: '1.1',
},
[mockPoofTokenId]: {
...mockTokenBalances[mockPoofTokenId],
balance: '5',
priceUsd: undefined,
},
},
},
}
const staleTokens = {
tokens: {
tokenBalances: {
[mockCeurTokenId]: {
...mockTokenBalances[mockCeurTokenId],
priceFetchedAt: Date.now() - ONE_DAY_IN_MILLIS,
balance: '5',
},
[mockEthTokenId]: {
...mockTokenBalances[mockEthTokenId],
priceFetchedAt: Date.now() - ONE_DAY_IN_MILLIS,
balance: '5',
},
},
},
}
jest.mocked(getMultichainFeatures).mockReturnValue({
showBalances: [
NetworkId['ethereum-sepolia'],
NetworkId['celo-alfajores'],
NetworkId['arbitrum-sepolia'],
NetworkId['op-sepolia'],
],
})
// Behavior specific to AssetsTokenBalance
describe('AssetsTokenBalance', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(getFeatureGate).mockReturnValue(true)
})
it.each([
{
testName: 'zero token balance and zero positions',
storeOverrides: {
...noTokens,
...noPositions,
},
expectedTotal: '$0.00',
},
{
testName: 'zero token balance and some positions',
storeOverrides: {
...noTokens,
},
expectedTotal: '$7.91',
},
{
testName: 'one token balance and some positions',
storeOverrides: {},
expectedTotal: '$8.41',
},
{
testName: 'one token balance and zero positions',
storeOverrides: {
...noPositions,
},
expectedTotal: '$0.50',
},
{
testName: 'multiple token balances (some with no price) and positions',
storeOverrides: {
...multipleTokens,
},
expectedTotal: '$1,666.03',
},
{
testName: 'stale token balance and positions',
storeOverrides: {
...staleTokens,
},
expectedTotal: '$-',
},
])('renders correctly with $testName', ({ storeOverrides, expectedTotal }) => {
const store = createMockStore({
...defaultStore,
...storeOverrides,
})
const tree = render(
)
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual(expectedTotal)
})
it.each([
{
testName: 'fetching the token balances failed and no positions',
storeOverrides: {
tokens: {
tokenBalances: {},
error: true,
},
...noPositions,
},
},
{
testName: 'fetching the token balances failed and some positions',
storeOverrides: {
tokens: {
tokenBalances: {},
error: true,
},
},
},
{
testName: 'fetching the local currency failed and no positions',
storeOverrides: {
...defaultStore,
localCurrency: {
error: true,
usdToLocalRate: null,
},
...noPositions,
},
},
{
testName: 'fetching the local currency failed and some positions',
storeOverrides: {
...defaultStore,
localCurrency: {
error: true,
usdToLocalRate: null,
},
},
},
])('renders error banner when $testName', ({ storeOverrides }) => {
const store = createMockStore(storeOverrides)
render(
)
expect(store.getActions()).toMatchInlineSnapshot(`
[
{
"action": {
"type": "HOME/REFRESH_BALANCES",
},
"alertType": "toast",
"buttonMessage": "outOfSyncBanner.button",
"dismissAfter": null,
"displayMethod": 0,
"message": "outOfSyncBanner.message",
"title": "outOfSyncBanner.title",
"type": "ALERT/SHOW",
"underlyingError": undefined,
},
]
`)
})
it('should show info on tap', () => {
const { getByText, getByTestId, queryByText } = render(
)
expect(getByTestId('TotalTokenBalance')).toHaveTextContent('₱55.74')
expect(queryByText('totalAssetsInfo')).toBeFalsy()
fireEvent.press(getByTestId('AssetsTokenBalance/Info'))
expect(getByText('totalAssetsInfo')).toBeTruthy()
})
it('renders title, balance and eye icon', () => {
const { getByText, getByTestId, queryByText } = render(
)
expect(getByTestId('EyeIcon')).toBeTruthy()
expect(getByText('bottomTabsNavigator.wallet.title')).toBeTruthy()
expect(getByTestId('TotalTokenBalance')).toHaveTextContent('₱55.74')
expect(queryByText('AssetsTokenBalance/Info')).toBeFalsy()
})
it('renders correctly when hideBalance is true and dispatches action on icon press', async () => {
const store = createMockStore({ ...defaultStore, app: { hideBalances: true } })
const tree = render(
)
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('XX.XX')
expect(tree.getByTestId('HiddenEyeIcon')).toBeTruthy()
fireEvent.press(tree.getByTestId('HiddenEyeIcon'))
expect(AppAnalytics.track).toHaveBeenCalledTimes(1)
expect(AppAnalytics.track).toHaveBeenCalledWith(HomeEvents.show_balances)
expect(store.getActions()).toEqual([hideAlert(), toggleHideBalances()])
})
it('renders correctly when hideBalance is false and dispatches action on icon press', async () => {
const store = createMockStore(defaultStore)
const tree = render(
)
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('$8.41')
expect(tree.getByTestId('EyeIcon')).toBeTruthy()
fireEvent.press(tree.getByTestId('EyeIcon'))
expect(AppAnalytics.track).toHaveBeenCalledTimes(1)
expect(AppAnalytics.track).toHaveBeenCalledWith(HomeEvents.hide_balances)
expect(store.getActions()).toEqual([hideAlert(), toggleHideBalances()])
})
})
// Behavior specific to FiatExchangeTokenBalance
describe('FiatExchangeTokenBalance', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(getFeatureGate).mockReturnValue(true)
})
it.each([
{
testName: 'zero token balance and zero positions',
storeOverrides: {
...noTokens,
...noPositions,
},
expectedTotal: '$0.00',
},
{
testName: 'zero token balance and some positions',
storeOverrides: {
...noTokens,
},
expectedTotal: '$7.91',
},
{
testName: 'one token balance and some positions',
storeOverrides: {},
expectedTotal: '$8.41',
},
])('renders correctly with $testName', ({ storeOverrides, expectedTotal }) => {
const store = createMockStore({
...defaultStore,
...storeOverrides,
})
const tree = render(
)
expect(tree.queryByTestId('ViewBalances')).toBeFalsy()
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual(expectedTotal)
})
it('includes token icon with one token balance and zero positions', () => {
const store = createMockStore({
...defaultStore,
...noPositions,
})
const tree = render(
)
expect(tree.queryByTestId('ViewBalances')).toBeFalsy()
expect(tree.getByTestId('TokenIcon')).toBeTruthy()
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('$0.50')
})
it('renders correctly with multiple token balances and positions and navigates to wallet tab if tab navigator gate is true', () => {
const store = createMockStore({
...defaultStore,
...multipleTokens,
})
const tree = render(
)
expect(tree.getByTestId('ViewBalances')).toBeTruthy()
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('$1,666.03')
fireEvent.press(tree.getByTestId('ViewBalances'))
expect(navigateClearingStack).toHaveBeenCalledWith(Screens.TabNavigator, {
initialScreen: Screens.TabWallet,
})
})
it('renders correctly with stale token balance and some positions', async () => {
const store = createMockStore(staleTokens)
const tree = render(
)
expect(tree.queryByTestId('ViewBalances')).toBeTruthy()
// Even we have positions, the balance is stale so we show '-'
expect(getElementText(tree.getByTestId('TotalTokenBalance'))).toEqual('₱-')
})
})