diff --git a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.test.tsx b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.test.tsx index 8e23e32446a6..34fd2eb73a99 100644 --- a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.test.tsx +++ b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.test.tsx @@ -13,7 +13,6 @@ import { AvatarAccountType } from '../../../components/Avatars/Avatar'; import { Maskicon } from '@metamask/design-system-react-native'; import JazzIcon from 'react-native-jazzicon'; import { Image as RNImage } from 'react-native'; -import { AccountCellIds } from '../../../../../e2e/selectors/MultichainAccounts/AccountCell.selectors'; // Configurable mock balance for selector const mockBalance: { value: number; currency: string } = { @@ -35,6 +34,18 @@ jest.mock('../../../../selectors/assets/balances', () => { }; }); +// Mock account selector to avoid deep store dependencies +jest.mock('../../../../selectors/multichainAccounts/accounts', () => { + const actual = jest.requireActual( + '../../../../selectors/multichainAccounts/accounts', + ); + return { + ...actual, + selectIconSeedAddressByAccountGroupId: () => () => + '0x1234567890abcdef1234567890abcdef12345678', + }; +}); + // Mock navigation const mockNavigate = jest.fn(); @@ -132,26 +143,23 @@ describe('AccountCell', () => { }); it('renders Maskicon AvatarAccount when avatarAccountType is Maskicon', () => { - const { getByTestId } = renderAccountCell({ + const { UNSAFE_getByType } = renderAccountCell({ avatarAccountType: AvatarAccountType.Maskicon, }); - const avatarContainer = getByTestId(AccountCellIds.AVATAR); - expect(() => avatarContainer.findByType(Maskicon)).not.toThrow(); + expect(UNSAFE_getByType(Maskicon)).toBeTruthy(); }); it('renders JazzIcon AvatarAccount when avatarAccountType is JazzIcon', () => { - const { getByTestId } = renderAccountCell({ + const { UNSAFE_getByType } = renderAccountCell({ avatarAccountType: AvatarAccountType.JazzIcon, }); - const avatarContainer = getByTestId(AccountCellIds.AVATAR); - expect(() => avatarContainer.findByType(JazzIcon)).not.toThrow(); + expect(UNSAFE_getByType(JazzIcon)).toBeTruthy(); }); it('renders Blockies AvatarAccount when avatarAccountType is Blockies', () => { - const { getByTestId } = renderAccountCell({ + const { UNSAFE_getByType } = renderAccountCell({ avatarAccountType: AvatarAccountType.Blockies, }); - const avatarContainer = getByTestId(AccountCellIds.AVATAR); - expect(() => avatarContainer.findByType(RNImage)).not.toThrow(); + expect(UNSAFE_getByType(RNImage)).toBeTruthy(); }); }); diff --git a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.tsx b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.tsx index 1e28562f04c1..467634e929ed 100644 --- a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.tsx +++ b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.tsx @@ -29,6 +29,7 @@ interface AccountCellProps { avatarAccountType: AvatarAccountType; isSelected: boolean; hideMenu?: boolean; + startAccessory?: React.ReactNode; } const AccountCell = ({ @@ -36,6 +37,7 @@ const AccountCell = ({ avatarAccountType, isSelected, hideMenu = false, + startAccessory, }: AccountCellProps) => { const { styles } = useStyles(styleSheet, { isSelected }); const { navigate } = useNavigation(); @@ -79,6 +81,7 @@ const AccountCell = ({ alignItems={AlignItems.center} testID={AccountCellIds.CONTAINER} > + {startAccessory} {accountGroup.metadata.name} - {isSelected && ( + {!startAccessory && isSelected && ( { onSelectAccount={mockOnSelectAccount} />, { - state: { - ...baseState, - settings: { avatarAccountType: 'Maskicon' }, - }, + state: baseState, }, ); expect(getByText('Test Account')).toBeTruthy(); @@ -69,10 +67,10 @@ describe('AccountListCell', () => { it('calls onSelectAccount when pressed', () => { const mockOnSelectAccount = jest.fn(); - const groups2 = [mockAccountGroup]; - const wallet2 = createMockWallet('test-group', 'Test Wallet', groups2); - const internalAccounts2 = createMockInternalAccountsFromGroups(groups2); - const baseState2 = createMockState([wallet2], internalAccounts2); + const groups = [mockAccountGroup]; + const wallet = createMockWallet('test-group', 'Test Wallet', groups); + const internalAccounts = createMockInternalAccountsFromGroups(groups); + const baseState = createMockState([wallet], internalAccounts); const { getByText } = renderWithProvider( { onSelectAccount={mockOnSelectAccount} />, { - state: { - ...baseState2, - settings: { avatarAccountType: 'Maskicon' }, - }, + state: baseState, }, ); // Given a rendered cell @@ -93,4 +88,165 @@ describe('AccountListCell', () => { fireEvent.press(getByText('Test Account')); expect(mockOnSelectAccount).toHaveBeenCalledWith(mockAccountGroup); }); + + describe('Checkbox functionality', () => { + let baseState: RootState; + + beforeEach(() => { + const groups = [mockAccountGroup]; + const wallet = createMockWallet('test-group', 'Test Wallet', groups); + const internalAccounts = createMockInternalAccountsFromGroups(groups); + baseState = createMockState([wallet], internalAccounts); + }); + + it('shows checkbox when showCheckbox prop is true', () => { + const mockOnSelectAccount = jest.fn(); + const { getByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + getByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeTruthy(); + }); + + it('hides checkbox when showCheckbox prop is false', () => { + const mockOnSelectAccount = jest.fn(); + const { queryByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + queryByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeFalsy(); + }); + + it('hides checkbox by default when showCheckbox prop is not provided', () => { + const mockOnSelectAccount = jest.fn(); + const { queryByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + queryByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeFalsy(); + }); + + it('renders checked checkbox when isSelected is true', () => { + const mockOnSelectAccount = jest.fn(); + const { getByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + getByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeTruthy(); + expect(getByTestId('checkbox-icon-component')).toBeTruthy(); + }); + + it('renders unchecked checkbox when isSelected is false', () => { + const mockOnSelectAccount = jest.fn(); + const { getByTestId, queryByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + getByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeTruthy(); + expect(queryByTestId('checkbox-icon-component')).toBeFalsy(); + }); + + it('calls onSelectAccount when checkbox is pressed', () => { + const mockOnSelectAccount = jest.fn(); + const { getByTestId } = renderWithProvider( + , + { state: baseState }, + ); + + const checkboxElement = getByTestId( + `account-list-cell-checkbox-${mockAccountGroup.id}`, + ); + fireEvent.press(checkboxElement); + + expect(mockOnSelectAccount).toHaveBeenCalledWith(mockAccountGroup); + }); + + it('calls onSelectAccount when TouchableOpacity container is pressed with checkbox enabled', () => { + const mockOnSelectAccount = jest.fn(); + const { getByText } = renderWithProvider( + , + { state: baseState }, + ); + + fireEvent.press(getByText('Test Account')); + + expect(mockOnSelectAccount).toHaveBeenCalledWith(mockAccountGroup); + }); + + it('renders AccountCell with correct props when checkbox is shown', () => { + const mockOnSelectAccount = jest.fn(); + const { getByTestId, getByText } = renderWithProvider( + , + { state: baseState }, + ); + + expect( + getByTestId(`account-list-cell-checkbox-${mockAccountGroup.id}`), + ).toBeTruthy(); + expect(getByTestId('checkbox-icon-component')).toBeTruthy(); + expect(getByText('Test Account')).toBeTruthy(); + }); + }); }); diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.tsx b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.tsx index 2d32976db961..bbf2e7de2bf3 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.tsx +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.tsx @@ -1,10 +1,11 @@ import React, { memo, useCallback } from 'react'; -import { TouchableOpacity } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { useStyles } from '../../../../hooks'; import AccountCell from '../../AccountCell'; import createStyles from '../MultichainAccountSelectorList.styles'; import { AccountListCellProps } from './AccountListCell.types'; +import Checkbox from '../../../../components/Checkbox'; const AccountListCell = memo( ({ @@ -12,6 +13,7 @@ const AccountListCell = memo( avatarAccountType, isSelected, onSelectAccount, + showCheckbox = false, }: AccountListCellProps) => { const { styles } = useStyles(createStyles, {}); @@ -26,6 +28,13 @@ const AccountListCell = memo( activeOpacity={0.7} > + + + ) : undefined + } accountGroup={accountGroup} avatarAccountType={avatarAccountType} isSelected={isSelected} diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.types.ts b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.types.ts index dc1975520dee..42ce8409517a 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.types.ts +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/AccountListCell/AccountListCell.types.ts @@ -6,4 +6,5 @@ export interface AccountListCellProps { avatarAccountType: AvatarAccountType; isSelected: boolean; onSelectAccount: (accountGroup: AccountGroupObject) => void; + showCheckbox?: boolean; } diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.test.tsx b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.test.tsx index 95b0dce2cc9d..15252c8a24ca 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.test.tsx +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.test.tsx @@ -774,4 +774,241 @@ describe('MultichainAccountSelectorList', () => { expect(queryByText('Account 1')).toBeFalsy(); }); }); + + describe('Checkbox functionality', () => { + it('shows checkboxes when showCheckbox prop is true', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [account1]); + const internalAccounts = createMockInternalAccountsFromGroups([account1]); + const mockState = createMockState([wallet1], internalAccounts); + + const { getByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + expect( + getByTestId(`account-list-cell-checkbox-${account1.id}`), + ).toBeTruthy(); + }); + + it('hides checkboxes when showCheckbox prop is false', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [account1]); + const internalAccounts = createMockInternalAccountsFromGroups([account1]); + const mockState = createMockState([wallet1], internalAccounts); + + const { queryByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + expect( + queryByTestId(`account-list-cell-checkbox-${account1.id}`), + ).toBeFalsy(); + }); + + it('displays checked checkbox for selected accounts', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const account2 = createMockAccountGroup( + 'keyring:wallet1/group2', + 'Account 2', + ['account2'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [ + account1, + account2, + ]); + const internalAccounts = createMockInternalAccountsFromGroups([ + account1, + account2, + ]); + const mockState = createMockState([wallet1], internalAccounts); + + const { getAllByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + // Check that checkboxes exist for both accounts + const account1Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account1.id}`, + ); + const account2Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account2.id}`, + ); + expect(account1Checkboxes.length).toEqual(1); // Only container should have the testID (selected account) + expect(account2Checkboxes.length).toEqual(1); // Only container should have the testID (unselected account) + + // Check that there is at least 1 checked checkbox icon (for the selected account) + // The checkbox icon uses the testID from the Checkbox component, which is overridden by the custom testID + // So we need to look for the actual checkbox elements with the custom testID + const selectedAccount = account1; // account1 is selected + const checkboxElements = getAllByTestId( + `account-list-cell-checkbox-${selectedAccount.id}`, + ); + expect(checkboxElements.length).toEqual(1); // Only container should have the testID (selected account) + }); + + it('displays unchecked checkbox for unselected accounts', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const account2 = createMockAccountGroup( + 'keyring:wallet1/group2', + 'Account 2', + ['account2'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [ + account1, + account2, + ]); + const internalAccounts = createMockInternalAccountsFromGroups([ + account1, + account2, + ]); + const mockState = createMockState([wallet1], internalAccounts); + + const { getAllByTestId, queryByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + // Check that checkboxes exist for both accounts + const account1Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account1.id}`, + ); + const account2Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account2.id}`, + ); + expect(account1Checkboxes.length).toEqual(1); // Only container (unselected account, no icon rendered) + expect(account2Checkboxes.length).toEqual(1); // Only container (unselected account, no icon rendered) + + // Check that there are no checked checkbox icons (since none are selected) + expect(queryByTestId('checkbox-icon-component')).toBeFalsy(); + }); + + it('calls onSelectAccount when checkbox is pressed', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [account1]); + const internalAccounts = createMockInternalAccountsFromGroups([account1]); + const mockState = createMockState([wallet1], internalAccounts); + + const { getAllByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + const checkboxElements = getAllByTestId( + `account-list-cell-checkbox-${account1.id}`, + ); + fireEvent.press(checkboxElements[0]); + + expect(mockOnSelectAccount).toHaveBeenCalledWith(account1); + }); + + it('shows checkboxes correctly with multiple selected accounts', () => { + const account1 = createMockAccountGroup( + 'keyring:wallet1/group1', + 'Account 1', + ['account1'], + ); + const account2 = createMockAccountGroup( + 'keyring:wallet1/group2', + 'Account 2', + ['account2'], + ); + const account3 = createMockAccountGroup( + 'keyring:wallet1/group3', + 'Account 3', + ['account3'], + ); + const wallet1 = createMockWallet('wallet1', 'Wallet 1', [ + account1, + account2, + account3, + ]); + const internalAccounts = createMockInternalAccountsFromGroups([ + account1, + account2, + account3, + ]); + const mockState = createMockState([wallet1], internalAccounts); + + const { getAllByTestId } = renderWithProvider( + , + { state: mockState }, + ); + + // Check that all 3 checkboxes exist + const account1Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account1.id}`, + ); + const account2Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account2.id}`, + ); + const account3Checkboxes = getAllByTestId( + `account-list-cell-checkbox-${account3.id}`, + ); + expect(account1Checkboxes.length).toEqual(1); // Only container should have the testID (selected account) + expect(account2Checkboxes.length).toEqual(1); // Only container should have the testID (unselected account) + expect(account3Checkboxes.length).toEqual(1); // Only container should have the testID (selected account) + + // Check that there are checked checkbox icons (for the 2 selected accounts) + // The checkbox icon uses the testID from the Checkbox component, which is overridden by the custom testID + // So we need to look for the actual checkbox elements with their custom testIDs + const selectedAccount1 = account1; // account1 is selected + const selectedAccount3 = account3; // account3 is selected + const checkboxElements1 = getAllByTestId( + `account-list-cell-checkbox-${selectedAccount1.id}`, + ); + const checkboxElements3 = getAllByTestId( + `account-list-cell-checkbox-${selectedAccount3.id}`, + ); + expect(checkboxElements1.length).toEqual(1); // Only container should have the testID (selected account) + expect(checkboxElements3.length).toEqual(1); // Only container should have the testID (selected account) + }); + }); }); diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.tsx b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.tsx index 9a07be7e41cf..affa433033f1 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.tsx +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.tsx @@ -40,6 +40,7 @@ const MultichainAccountSelectorList = ({ selectedAccountGroups, testID = MULTICHAIN_ACCOUNT_SELECTOR_LIST_TESTID, listRef, + showCheckbox = false, ...props }: MultichainAccountSelectorListProps) => { const { styles } = useStyles(createStyles, {}); @@ -229,6 +230,7 @@ const MultichainAccountSelectorList = ({ avatarAccountType={avatarAccountType} isSelected={isSelected} onSelectAccount={handleSelectAccount} + showCheckbox={showCheckbox} /> ); } @@ -251,6 +253,7 @@ const MultichainAccountSelectorList = ({ handleSelectAccount, handleAccountCreated, avatarAccountType, + showCheckbox, ], ); diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.types.ts b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.types.ts index 1becc8e2bd4c..0ebd93e9803c 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.types.ts +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.types.ts @@ -32,6 +32,10 @@ export interface MultichainAccountSelectorListProps * Reference to the FlashList component */ listRef?: RefObject>; + /** + * Optional boolean to show checkbox + */ + showCheckbox?: boolean; } /** diff --git a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx index d1dc1f69ec6c..7d8bac92bcb4 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx +++ b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx @@ -171,6 +171,7 @@ const MultichainAccountConnectMultiSelector = ({ onSelectAccount={onSelectAccountGroupId} selectedAccountGroups={selectedAccountGroups} testID={AccountListBottomSheetSelectorsIDs.ACCOUNT_LIST_ID} + showCheckbox /> {connection?.originatorInfo?.apiVersion && ( diff --git a/app/components/Views/MultichainAccounts/MultichainAccountPermissions/MultichainAccountPermissions.tsx b/app/components/Views/MultichainAccounts/MultichainAccountPermissions/MultichainAccountPermissions.tsx index 8a5dd357806d..8b58ffecdb36 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountPermissions/MultichainAccountPermissions.tsx +++ b/app/components/Views/MultichainAccounts/MultichainAccountPermissions/MultichainAccountPermissions.tsx @@ -348,7 +348,7 @@ export const MultichainAccountPermissions = ( screenTitle={strings('accounts.edit_accounts_title')} showDisconnectAllButton onUserAction={() => { - // TODO: Implement user action handler + // Not used in this component. }} isRenderedAsBottomSheet={false} /> diff --git a/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.tsx b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.tsx index f08654fd1537..fffb29c53cb3 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.tsx +++ b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.tsx @@ -38,8 +38,8 @@ const MultichainAccountsConnectedList = ({ const renderItem = useCallback( ({ item }: { item: AccountGroupObject }) => ( { // No op here because it is handled by edit accounts. }} diff --git a/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap index 4bce63edc6ba..fbe3347c1af2 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap +++ b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap @@ -123,7 +123,7 @@ exports[`MultichainAccountsConnectedList renders component with different accoun style={ { "alignItems": "center", - "borderColor": "#4459ff", + "borderColor": "transparent", "borderRadius": 8, "borderWidth": 2, "height": 36, @@ -183,20 +183,6 @@ exports[`MultichainAccountsConnectedList renders component with different accoun > Account 1 - Account 2 -