diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 0cb8daa6c9bf..306c88825c40 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -8,7 +8,12 @@ import { View } from 'react-native'; import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; import { backgroundState } from '../../../util/test/initial-root-state'; import { regex } from '../../../../app/util/regex'; -import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; +import { + createMockAccountsControllerState, + createMockAccountsControllerStateWithSnap, + MOCK_ADDRESS_1, + MOCK_ADDRESS_2, +} from '../../../util/test/accountsControllerTestUtils'; import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; @@ -200,4 +205,28 @@ describe('AccountSelectorList', () => { expect(within(accountNameItems[1]).getByText('Account 2')).toBeDefined(); }); }); + it('renders "Snaps (beta)" tag for Snap accounts', async () => { + const mockAccountsWithSnap = createMockAccountsControllerStateWithSnap([ + MOCK_ADDRESS_1, + MOCK_ADDRESS_2, + ]); + + const stateWithSnapAccount = { + ...initialState, + engine: { + ...initialState.engine, + backgroundState: { + ...initialState.engine.backgroundState, + AccountsController: mockAccountsWithSnap, + }, + }, + }; + + const { queryByText } = renderComponent(stateWithSnapAccount); + + await waitFor(async () => { + const snapTag = await queryByText('Snaps (beta)'); + expect(snapTag).toBeDefined(); + }); + }); }); diff --git a/app/core/__mocks__/MockedEngine.ts b/app/core/__mocks__/MockedEngine.ts index 104f346e2be5..dcdde0ec33c9 100644 --- a/app/core/__mocks__/MockedEngine.ts +++ b/app/core/__mocks__/MockedEngine.ts @@ -1,38 +1,13 @@ -import { KeyringTypes } from '@metamask/keyring-controller'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { mockNetworkState } from '../../util/test/network'; import { NetworkClientId } from '@metamask/network-controller'; import Engine from '../../core/Engine'; +import { MOCK_KEYRING_CONTROLLER_STATE } from '../../util/test/keyringControllerTestUtils'; export const mockedEngine = { init: () => Engine.init({}), context: { - KeyringController: { - keyring: { - keyrings: [ - { - mnemonic: - 'one two three four five six seven eight nine ten eleven twelve', - }, - ], - }, - state: { - keyrings: [ - { - accounts: ['0xd018538C87232FF95acbCe4870629b75640a78E7'], - type: KeyringTypes.simple, - }, - { - accounts: ['0xB374Ca013934e498e5baD3409147F34E6c462389'], - type: KeyringTypes.qr, - }, - { - accounts: ['0x71C7656EC7ab88b098defB751B7401B5f6d8976F'], - type: KeyringTypes.hd, - }, - ], - }, - }, + KeyringController: MOCK_KEYRING_CONTROLLER_STATE, NetworkController: { getNetworkClientById: (networkClientId: NetworkClientId) => { if (networkClientId === 'linea_goerli') { diff --git a/app/util/address/index.test.ts b/app/util/address/index.test.ts index e19035c34d29..763059d0ba5c 100644 --- a/app/util/address/index.test.ts +++ b/app/util/address/index.test.ts @@ -15,6 +15,37 @@ import { getKeyringByAddress, getLabelTextByAddress, } from '.'; +import { + mockHDKeyringAddress, + mockQrKeyringAddress, + mockSimpleKeyringAddress, +} from '../test/keyringControllerTestUtils'; + +const snapAddress = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; + +jest.mock('../../core/Engine', () => { + const { KeyringTypes } = jest.requireActual('@metamask/keyring-controller'); + const { MOCK_KEYRING_CONTROLLER_STATE } = jest.requireActual( + '../test/keyringControllerTestUtils', + ); + return { + context: { + KeyringController: { + ...MOCK_KEYRING_CONTROLLER_STATE, + state: { + keyrings: [ + ...MOCK_KEYRING_CONTROLLER_STATE.state.keyrings, + { + accounts: [snapAddress], + index: 0, + type: KeyringTypes.snap, + }, + ], + }, + }, + }, + }; +}); describe('isENS', () => { it('should return false by default', () => { @@ -232,19 +263,13 @@ describe('isQRHardwareAccount', () => { }); it('should return false if address is from keyring type simple', () => { - expect( - isQRHardwareAccount('0xd018538C87232FF95acbCe4870629b75640a78E7'), - ).toBeFalsy(); + expect(isQRHardwareAccount(mockSimpleKeyringAddress)).toBeFalsy(); }); it('should return false if address is from keyring type hd', () => { - expect( - isQRHardwareAccount('0x71C7656EC7ab88b098defB751B7401B5f6d8976F'), - ).toBeFalsy(); + expect(isQRHardwareAccount(mockHDKeyringAddress)).toBeFalsy(); }); it('should return true if address is from keyring type qr', () => { - expect( - isQRHardwareAccount('0xB374Ca013934e498e5baD3409147F34E6c462389'), - ).toBeTruthy(); + expect(isQRHardwareAccount(mockQrKeyringAddress)).toBeTruthy(); }); }); describe('getKeyringByAddress', () => { @@ -257,9 +282,7 @@ describe('getKeyringByAddress', () => { expect(getKeyringByAddress('ens.eth')).toBeUndefined(); }); it('should return address if found', () => { - expect( - getKeyringByAddress('0xB374Ca013934e498e5baD3409147F34E6c462389'), - ).not.toBe(undefined); + expect(getKeyringByAddress(mockQrKeyringAddress)).not.toBe(undefined); }); it('should return null if address not found', () => { expect( @@ -269,9 +292,7 @@ describe('getKeyringByAddress', () => { }); describe('isHardwareAccount,', () => { it('should return true if account is a QR keyring', () => { - expect( - isHardwareAccount('0xB374Ca013934e498e5baD3409147F34E6c462389'), - ).toBeTruthy(); + expect(isHardwareAccount(mockQrKeyringAddress)).toBeTruthy(); }); it('should return false if account is not a hardware keyring', () => { @@ -281,16 +302,26 @@ describe('isHardwareAccount,', () => { }); }); describe('getLabelTextByAddress,', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('should return accounts.qr_hardware if account is a QR keyring', () => { - expect( - getLabelTextByAddress('0xB374Ca013934e498e5baD3409147F34E6c462389'), - ).toBe('accounts.qr_hardware'); + expect(getLabelTextByAddress(mockQrKeyringAddress)).toBe( + 'accounts.qr_hardware', + ); }); it('should return KeyringTypes.simple if address is a imported account', () => { - expect( - getLabelTextByAddress('0xd018538C87232FF95acbCe4870629b75640a78E7'), - ).toBe('accounts.imported'); + expect(getLabelTextByAddress(mockSimpleKeyringAddress)).toBe( + 'accounts.imported', + ); + }); + + it('returns "Snaps (beta)" if account is a Snap keyring', () => { + expect(getLabelTextByAddress(snapAddress)).toBe( + 'accounts.snap_account_tag', + ); }); it('should return null if address is empty', () => { @@ -312,19 +343,13 @@ describe('getAddressAccountType', () => { ); }); it('should return QR if address is from a keyring type qr', () => { - expect( - getAddressAccountType('0xB374Ca013934e498e5baD3409147F34E6c462389'), - ).toBe('QR'); + expect(getAddressAccountType(mockQrKeyringAddress)).toBe('QR'); }); it('should return imported if address is from a keyring type simple', () => { - expect( - getAddressAccountType('0xd018538C87232FF95acbCe4870629b75640a78E7'), - ).toBe('Imported'); + expect(getAddressAccountType(mockSimpleKeyringAddress)).toBe('Imported'); }); it('should return MetaMask if address is not qr or simple', () => { - expect( - getAddressAccountType('0x71C7656EC7ab88b098defB751B7401B5f6d8976F'), - ).toBe('MetaMask'); + expect(getAddressAccountType(mockHDKeyringAddress)).toBe('MetaMask'); }); }); describe('resemblesAddress', () => { @@ -337,8 +362,6 @@ describe('resemblesAddress', () => { expect(resemblesAddress('address-stub-1')).toBeFalsy(); }); it('should return true if address resemble an eth address', () => { - expect( - resemblesAddress('0x71C7656EC7ab88b098defB751B7401B5f6d8976F'), - ).toBeTruthy(); + expect(resemblesAddress(mockHDKeyringAddress)).toBeTruthy(); }); }); diff --git a/app/util/address/index.ts b/app/util/address/index.ts index 3d8bf93f0014..6d333d76f9e2 100644 --- a/app/util/address/index.ts +++ b/app/util/address/index.ts @@ -37,7 +37,12 @@ import { InternalAccount } from '@metamask/keyring-api'; import { AddressBookControllerState } from '@metamask/address-book-controller'; import { NetworkType, toChecksumHexAddress } from '@metamask/controller-utils'; import { NetworkClientId, NetworkState } from '@metamask/network-controller'; -import { AccountImportStrategy } from '@metamask/keyring-controller'; +import { + AccountImportStrategy, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + KeyringTypes, + ///: END:ONLY_INCLUDE_IF +} from '@metamask/keyring-controller'; import { Hex, isHexString } from '@metamask/utils'; const { @@ -244,6 +249,10 @@ export function getLabelTextByAddress(address: string) { return 'accounts.qr_hardware'; case ExtendedKeyringTypes.simple: return 'accounts.imported'; + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + case KeyringTypes.snap: + return 'accounts.snap_account_tag'; + ///: END:ONLY_INCLUDE_IF } } return null; diff --git a/app/util/test/accountsControllerTestUtils.ts b/app/util/test/accountsControllerTestUtils.ts index 8a89adf070f1..983a0ba94f97 100644 --- a/app/util/test/accountsControllerTestUtils.ts +++ b/app/util/test/accountsControllerTestUtils.ts @@ -1,6 +1,7 @@ import { v4 as uuidV4 } from 'uuid'; import { EthMethod, InternalAccount } from '@metamask/keyring-api'; import { AccountsControllerState } from '@metamask/accounts-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; export function createMockUuidFromAddress(address: string): string { const fakeShaFromAddress = Array.from( @@ -99,3 +100,30 @@ export function createMockAccountsControllerState( }, }; } + +export function createMockAccountsControllerStateWithSnap( + addresses: string[], + snapAccountIndex: number = 0, +): AccountsControllerState { + if (addresses.length === 0) { + throw new Error('At least one address is required'); + } + + if (snapAccountIndex < 0 || snapAccountIndex >= addresses.length) { + throw new Error('Invalid snapAccountIndex'); + } + + const state = createMockAccountsControllerState( + addresses, + addresses[snapAccountIndex], + ); + + const snapAccountUuid = createMockUuidFromAddress( + addresses[snapAccountIndex].toLowerCase(), + ); + state.internalAccounts.accounts[snapAccountUuid].metadata.keyring = { + type: KeyringTypes.snap, + }; + + return state; +} diff --git a/app/util/test/keyringControllerTestUtils.ts b/app/util/test/keyringControllerTestUtils.ts new file mode 100644 index 000000000000..4e5075cef4b4 --- /dev/null +++ b/app/util/test/keyringControllerTestUtils.ts @@ -0,0 +1,37 @@ +import { KeyringObject, KeyringTypes } from '@metamask/keyring-controller'; + +export const mockSimpleKeyringAddress = + '0xd018538C87232FF95acbCe4870629b75640a78E7'; +export const mockQrKeyringAddress = + '0xB374Ca013934e498e5baD3409147F34E6c462389'; +export const mockHDKeyringAddress = + '0x71C7656EC7ab88b098defB751B7401B5f6d8976F'; + +const MOCK_DEFAULT_KEYRINGS: KeyringObject[] = [ + { + accounts: [mockSimpleKeyringAddress], + type: KeyringTypes.simple, + }, + { + accounts: [mockQrKeyringAddress], + type: KeyringTypes.qr, + }, + { + accounts: [mockHDKeyringAddress], + type: KeyringTypes.hd, + }, +]; + +export const MOCK_KEYRING_CONTROLLER_STATE = { + keyring: { + keyrings: [ + { + mnemonic: + 'one two three four five six seven eight nine ten eleven twelve', + }, + ], + }, + state: { + keyrings: MOCK_DEFAULT_KEYRINGS, + }, +}; diff --git a/bitrise.yml b/bitrise.yml index a30098cbeaad..cb3f198babb8 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -644,8 +644,8 @@ workflows: android_e2e_test: before_run: - setup - - install_detox - prep_environment + - install_detox after_run: - notify_failure steps: @@ -737,8 +737,8 @@ workflows: ios_api_specs: before_run: - setup - - install_detox - prep_environment + - install_detox after_run: - notify_failure steps: @@ -890,8 +890,8 @@ workflows: ios_e2e_test: before_run: - setup - - install_detox - prep_environment + - install_detox after_run: - notify_failure steps: diff --git a/sonar-project.properties b/sonar-project.properties index 32b328b42e6b..cb542a3ba9c8 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -15,7 +15,7 @@ sonar.exclusions=**.stories.**, e2e/**, wdio/** sonar.test.inclusions=**.test.** # Excluded project files from coverage. -sonar.coverage.exclusions=**AccountConnectMultiSelector**, **AccountPermissionsConfirmRevokeAll**, **PermissionsSettings**, **NetworkConnectMultiSelector**, **NetworkSelectorList**, **PermissionsSummary**, **AccountConnect.tsx +sonar.coverage.exclusions=**AccountConnectMultiSelector**, **AccountPermissionsConfirmRevokeAll**, **PermissionsSettings**, **NetworkConnectMultiSelector**, **NetworkSelectorList**, **PermissionsSummary**, **AccountConnect.tsx, **util/test** # Test coverage path in GitHub action sonar.javascript.lcov.reportPaths=/coverage/lcov.info