Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/e2e/flask/multi-srp/import-srp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Multi SRP - Import SRP', function (this: Suite) {
},
async (driver: Driver) => {
const accountListPage = new AccountListPage(driver);
await accountListPage.checkAccountBelongsToSrp('Account 2', 2);
await accountListPage.checkAccountBelongsToSrp('Account 1', 2);
},
);
});
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/page-objects/pages/account-list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,14 @@ class AccountListPage {
if (srpIndex === 0) {
throw new Error('SRP index must be > 0');
}

// Wait for the expected number of SRP cards to be rendered
// This is needed because the accountTree is updated asynchronously after importing a new SRP
await this.driver.wait(async () => {
const srps = await this.driver.findElements('.select-srp__container');
return srps.length >= srpIndex;
}, 10000);

const srps = await this.driver.findElements('.select-srp__container');
const selectedSrp = srps[srpIndex - 1];
const showAccountsButton = await this.driver.waitForSelector(
Expand Down
2 changes: 1 addition & 1 deletion ui/components/multichain/multi-srp/srp-list/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
&__account-name {
overflow: hidden;
white-space: nowrap;
max-width: 80px;
max-width: 120px;
}
}
1 change: 1 addition & 0 deletions ui/components/multichain/multi-srp/srp-list/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SrpList } from './srp-list';
export { SrpListItem } from './srp-list-item';
99 changes: 99 additions & 0 deletions ui/components/multichain/multi-srp/srp-list/srp-card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fireEvent } from '@testing-library/react';

import { KeyringTypes } from '@metamask/keyring-controller';
import type { AccountGroupId, AccountWalletId } from '@metamask/account-api';

import { renderWithProvider } from '../../../../../test/lib/render-helpers-navigate';
import mockState from '../../../../../test/data/mock-state.json';
import { FirstTimeFlowType } from '../../../../../shared/constants/onboarding';
import { SrpCard } from './srp-card';

const mockWalletId = 'mock-wallet-id' as AccountWalletId;
const mockTotalFiatBalance = '$100.00';

const mocks = {
useSingleWalletAccountsBalanceCallback: jest
.fn()
.mockReturnValue((_: AccountGroupId) => mockTotalFiatBalance),
onActionComplete: jest.fn(),
useWalletInfoCallback: jest.fn().mockReturnValue({
multichainAccounts: [
{
id: 'mock-account-id-1' as AccountGroupId,
metadata: {
name: 'Mock Account 1',
},
},
],
keyringId: '01JKAF3DSGM3AB87EM9N0K41AJ',
isSRPBackedUp: true,
}),
};

jest.mock('../../../../hooks/multichain-accounts/useWalletBalance', () => ({
useSingleWalletAccountsBalanceCallback: (walletId: AccountWalletId) =>
mocks.useSingleWalletAccountsBalanceCallback(walletId),
}));

jest.mock('../../../../hooks/multichain-accounts/useWalletInfo', () => ({
useWalletInfo: (walletId: AccountWalletId) =>
mocks.useWalletInfoCallback(walletId),
}));

const mockSecondHdKeyring = {
accounts: [],
type: KeyringTypes.hd,
metadata: {
id: '01JN31PKMJ3ANWYFJZM3Z8MYT4',
name: '',
},
};

const render = (shouldTriggerBackup: boolean) => {
const store = configureMockStore([thunk])({
...mockState,
metamask: {
...mockState.metamask,
keyrings: [...mockState.metamask.keyrings, mockSecondHdKeyring],
firstTimeFlowType: FirstTimeFlowType.create,
seedPhraseBackedUp: false,
},
});

return renderWithProvider(
<SrpCard
index={0}
walletId={mockWalletId}
shouldTriggerBackup={shouldTriggerBackup}
onActionComplete={mocks.onActionComplete}
/>,
store,
);
};

describe('SrpCard', () => {
it('renders the secret recovery phrases card', () => {
const { getByText } = render(false);
expect(getByText('Secret Recovery Phrase 1')).toBeInTheDocument();
});

it('shows/hides accounts when clicking show/hide text', () => {
const { getByText } = render(false);
const showAccountsButton = getByText('Show 1 account');
fireEvent.click(showAccountsButton);
expect(getByText('Hide 1 account')).toBeInTheDocument();
});

it('calls onActionComplete when clicking a keyring', () => {
const { getByTestId } = render(true);
const firstKeyringId = mockState.metamask.keyrings[0].metadata.id;

const keyring = getByTestId(`hd-keyring-${firstKeyringId}`);
fireEvent.click(keyring);

expect(mocks.onActionComplete).toHaveBeenCalledWith(firstKeyringId, true);
});
});
174 changes: 174 additions & 0 deletions ui/components/multichain/multi-srp/srp-list/srp-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useState, useContext, useCallback } from 'react';
import type { AccountWalletId } from '@metamask/account-api';

import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { useWalletInfo } from '../../../../hooks/multichain-accounts/useWalletInfo';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { useSingleWalletAccountsBalanceCallback } from '../../../../hooks/multichain-accounts/useWalletBalance';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';

import Card from '../../../ui/card';
import {
Box,
IconName,
Icon,
Text,
IconSize,
} from '../../../component-library';
import {
JustifyContent,
Display,
TextColor,
FlexDirection,
AlignItems,
BlockSize,
TextVariant,
IconColor,
} from '../../../../helpers/constants/design-system';
import { SrpListItem } from './srp-list-item';

/**
* Props for the SrpCard component.
*/
type SrpCardProps = {
index: number;
walletId: AccountWalletId;
shouldTriggerBackup: boolean;
onActionComplete: (id: string, triggerBackup?: boolean) => void;
isSettingsPage?: boolean;
hideShowAccounts?: boolean;
};

export const SrpCard = ({
index,
walletId,
shouldTriggerBackup,
onActionComplete,
isSettingsPage = false,
hideShowAccounts = false,
}: SrpCardProps) => {
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const { multichainAccounts, keyringId } = useWalletInfo(walletId);
const [showAccounts, setShowAccounts] = useState<boolean>(false);
const walletAccountBalance = useSingleWalletAccountsBalanceCallback(walletId);

const showHideText = useCallback(
(numberOfAccounts: number): string => {
if (numberOfAccounts > 1) {
return showAccounts
? t('SrpListHideAccounts', [numberOfAccounts])
: t('SrpListShowAccounts', [numberOfAccounts]);
}
return showAccounts
? t('SrpListHideSingleAccount', [numberOfAccounts])
: t('SrpListShowSingleAccount', [numberOfAccounts]);
},
[showAccounts, t],
);

return (
<Card
key={`srp-${index}-${keyringId}`}
data-testid={`hd-keyring-${keyringId}`}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Accounts,
event: MetaMetricsEventName.SecretRecoveryPhrasePickerClicked,
properties: {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
button_type: 'srp_select',
},
});
keyringId && onActionComplete(keyringId, shouldTriggerBackup);
}}
className="select-srp__container"
marginBottom={3}
>
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
>
<Box>
<Text variant={TextVariant.bodyMdMedium}>
{t('srpListName', [index + 1])}
</Text>
{!hideShowAccounts && (
<Text
variant={TextVariant.bodySm}
color={TextColor.primaryDefault}
className="srp-list__show-accounts"
data-testid={`srp-list-show-accounts-${index}`}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
trackEvent({
category: MetaMetricsEventCategory.Accounts,
event: MetaMetricsEventName.SecretRecoveryPhrasePickerClicked,
properties: {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
button_type: 'details',
},
});
setShowAccounts((prevState) => !prevState);
}}
>
{showHideText(multichainAccounts.length)}
</Text>
)}
</Box>
<Box display={Display.Flex} alignItems={AlignItems.center} gap={1}>
{isSettingsPage && (
<Text
variant={TextVariant.bodyMdMedium}
color={
shouldTriggerBackup
? TextColor.errorDefault
: TextColor.textAlternative
}
>
{shouldTriggerBackup
? t('srpListStateNotBackedUp')
: t('srpListStateBackedUp')}
</Text>
)}
<Icon
name={IconName.ArrowRight}
size={IconSize.Sm}
color={
shouldTriggerBackup && isSettingsPage
? IconColor.errorDefault
: IconColor.iconAlternative
}
/>
</Box>
</Box>
{showAccounts && (
<Box>
<Box
width={BlockSize.Full}
className="srp-list__divider"
marginTop={2}
marginBottom={2}
/>
{multichainAccounts.map((group) => {
return (
<SrpListItem
key={`account-${group.id}`}
accountId={group.id}
accountName={group.metadata.name}
balance={walletAccountBalance(group.id) ?? ''}
/>
);
})}
</Box>
)}
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -1,65 +1,42 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { type AccountGroupId } from '@metamask/account-api';

import mockState from '../../../../../test/data/mock-state.json';
import { createMockInternalAccount } from '../../../../../test/jest/mocks';
import { InternalAccountWithBalance } from '../../../../selectors';
import { renderWithProvider } from '../../../../../test/lib/render-helpers-navigate';
import { shortenAddress } from '../../../../helpers/utils/util';
import { SrpListItem } from './srp-list-item';

const mockTotalFiatBalance = '100';
const mockAccount: InternalAccountWithBalance = {
...createMockInternalAccount({
name: 'Test Account',
address: '0xB1BAF6A2f4A808937bb97a2F12CCF08F1233e3D9',
}),
balance: mockTotalFiatBalance,
};

const mocks = {
useMultichainAccountTotalFiatBalance: jest.fn().mockReturnValue({
totalFiatBalance: mockTotalFiatBalance,
}),
};

jest.mock('../../../../hooks/useMultichainAccountTotalFiatBalance', () => ({
useMultichainAccountTotalFiatBalance: (account: InternalAccountWithBalance) =>
mocks.useMultichainAccountTotalFiatBalance(account),
}));
jest.mock('../../../../helpers/utils/util', () => ({
...jest.requireActual('../../../../helpers/utils/util'),
}));
const mockAccountId = 'mock-account-id' as AccountGroupId;
const mockAccountName = 'Mock Account Name';
const mockBalance = '$100.00';

const render = () => {
const store = configureMockStore([thunk])(mockState);
return renderWithProvider(<SrpListItem account={mockAccount} />, store);
return renderWithProvider(
<SrpListItem
accountId={mockAccountId}
accountName={mockAccountName}
balance={mockBalance}
/>,
store,
);
};

describe('SrpListItem', () => {
beforeEach(() => {
// Reset mock implementations before each test
mocks.useMultichainAccountTotalFiatBalance.mockReturnValue({
totalFiatBalance: mockTotalFiatBalance,
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('renders account name and shortened address', () => {
it('renders account name', () => {
const { getByText } = render();

expect(getByText(mockAccount.metadata.name)).toBeInTheDocument();
expect(getByText(shortenAddress(mockAccount.address))).toBeInTheDocument();
expect(getByText(mockAccountName)).toBeInTheDocument();
});

it('calls useMultichainAccountTotalFiatBalance with correct account', () => {
render();
const { getByText } = render();

expect(mocks.useMultichainAccountTotalFiatBalance).toHaveBeenCalledWith(
mockAccount,
);
expect(getByText(mockBalance)).toBeInTheDocument();
});
});
Loading
Loading