Skip to content

Commit 1d85b91

Browse files
authored
feat: Add search functionality to the Multichain Account List page (#35616)
## **Description** This PR adds search functionality implementation for the Multichain Account List. Notes: - Improvement for hiding wallet header for singular wallet is added (it was initial requirement for the list). - Create new account button is removed in search mode, as it doesn't make much sense to create account which name will be filtered already (not visible). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/35616?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Added search functionality to the Multichain Account List ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUL-370 ## **Manual testing steps** 1. Go to multichain account list page while multichain state 2 feature flag is enabled. 2. Test search by typing account names in the search bar. ## **Screenshots/Recordings** ### **Before** Search was not available before. Nothing to show here. ### **After** <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 25 22" src="https://github.com/user-attachments/assets/ed2d682d-2a31-488d-8ab6-0a8edf703570" /> <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 25 40" src="https://github.com/user-attachments/assets/1c0faaf4-51b6-473d-ace8-02a07df9e12a" /> <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 26 43" src="https://github.com/user-attachments/assets/3ade76ca-304a-4b13-9962-a2745c66f50d" /> <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 25 02" src="https://github.com/user-attachments/assets/70e1ab83-6a12-4293-b8d0-f5190bac9930" /> <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 26 02" src="https://github.com/user-attachments/assets/3b782a0e-3722-40ae-8724-b3e251f6b8ef" /> <img width="398" height="602" alt="Screenshot 2025-09-04 at 12 26 34" src="https://github.com/user-attachments/assets/01d5dcd1-6c0a-4fc6-841e-780c817756e0" /> https://github.com/user-attachments/assets/84ad68a1-4445-4d2a-bc5e-99bacdb63741 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent d6c4d26 commit 1d85b91

File tree

8 files changed

+323
-56
lines changed

8 files changed

+323
-56
lines changed

app/_locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/components/multichain-accounts/multichain-account-list/multichain-account-list.test.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ describe('MultichainAccountList', () => {
153153
expect(screen.getByText('Account 1 from wallet 2')).toBeInTheDocument();
154154
});
155155

156+
it('does not render wallet headers based on prop', () => {
157+
renderComponent({ displayWalletHeader: false });
158+
159+
expect(screen.queryByText('Wallet 1')).not.toBeInTheDocument();
160+
expect(screen.queryByText('Wallet 2')).not.toBeInTheDocument();
161+
expect(
162+
screen.getByTestId(`multichain-account-cell-${walletOneGroupId}`),
163+
).toBeInTheDocument();
164+
expect(
165+
screen.getByTestId(`multichain-account-cell-${walletTwoGroupId}`),
166+
).toBeInTheDocument();
167+
168+
expect(screen.getByText('Account 1 from wallet 1')).toBeInTheDocument();
169+
expect(screen.getByText('Account 1 from wallet 2')).toBeInTheDocument();
170+
});
171+
156172
it('marks only the selected account with a check icon and dispatches action on click', () => {
157173
renderComponent();
158174

@@ -237,7 +253,7 @@ describe('MultichainAccountList', () => {
237253
renderComponent({ wallets: multiGroupWallets });
238254

239255
expect(
240-
screen.getAllByTestId('multichain-account-tree-wallet-header'),
256+
screen.queryAllByTestId('multichain-account-tree-wallet-header'),
241257
).toHaveLength(1);
242258
expect(
243259
screen.getByTestId(`multichain-account-cell-${walletOneGroupId}`),

ui/components/multichain-accounts/multichain-account-list/multichain-account-list.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ export type MultichainAccountListProps = {
4141
wallets: AccountTreeWallets;
4242
selectedAccountGroups: AccountGroupId[];
4343
handleAccountClick?: (accountGroupId: AccountGroupId) => void;
44+
isInSearchMode?: boolean;
45+
displayWalletHeader?: boolean;
4446
};
4547

4648
export const MultichainAccountList = ({
4749
wallets,
4850
selectedAccountGroups,
4951
handleAccountClick,
52+
isInSearchMode = false,
53+
displayWalletHeader = true,
5054
}: MultichainAccountListProps) => {
5155
const dispatch = useDispatch();
5256
const history = useHistory();
@@ -144,7 +148,7 @@ export const MultichainAccountList = ({
144148
},
145149
);
146150

147-
if (walletData.type === AccountWalletType.Entropy) {
151+
if (!isInSearchMode && walletData.type === AccountWalletType.Entropy) {
148152
groupsItems.push(
149153
<AddMultichainAccount
150154
walletId={walletId as AccountWalletId}
@@ -153,7 +157,11 @@ export const MultichainAccountList = ({
153157
);
154158
}
155159

156-
return [...walletsAccumulator, walletHeader, ...groupsItems];
160+
return [
161+
...walletsAccumulator,
162+
displayWalletHeader ? walletHeader : null,
163+
...groupsItems,
164+
];
157165
},
158166
[] as React.ReactNode[],
159167
);
@@ -165,6 +173,8 @@ export const MultichainAccountList = ({
165173
defaultHomeActiveTabName,
166174
dispatch,
167175
history,
176+
isInSearchMode,
177+
displayWalletHeader,
168178
selectedAccountGroupsSet,
169179
]);
170180

Lines changed: 90 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { screen, fireEvent } from '@testing-library/react';
2+
import { screen, fireEvent, within } from '@testing-library/react';
33

44
import { renderWithProvider } from '../../../../test/lib/render-helpers';
55
import mockState from '../../../../test/data/mock-state.json';
@@ -17,6 +17,10 @@ jest.mock('react-router-dom', () => ({
1717
}),
1818
}));
1919

20+
const searchContainerTestId = 'multichain-account-list-search';
21+
const searchClearButtonTestId = 'text-field-search-clear-button';
22+
const walletHeaderTestId = 'multichain-account-tree-wallet-header';
23+
2024
describe('AccountList', () => {
2125
beforeEach(() => {
2226
jest.clearAllMocks();
@@ -26,44 +30,6 @@ describe('AccountList', () => {
2630
const store = configureStore({
2731
metamask: {
2832
...mockState.metamask,
29-
accountTree: {
30-
selectedAccountGroup: '01JKAF3DSGM3AB87EM9N0K41AJ:default',
31-
wallets: {
32-
'01JKAF3DSGM3AB87EM9N0K41AJ': {
33-
id: '01JKAF3DSGM3AB87EM9N0K41AJ',
34-
metadata: {
35-
name: 'Wallet 1',
36-
},
37-
groups: {
38-
'01JKAF3DSGM3AB87EM9N0K41AJ:default': {
39-
id: '01JKAF3DSGM3AB87EM9N0K41AJ:default',
40-
metadata: {
41-
name: 'Account 1 from wallet 1',
42-
},
43-
accounts: [
44-
'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3',
45-
'07c2cfec-36c9-46c4-8115-3836d3ac9047',
46-
],
47-
},
48-
},
49-
},
50-
'01JKAF3PJ247KAM6C03G5Q0NP8': {
51-
id: '01JKAF3PJ247KAM6C03G5Q0NP8',
52-
metadata: {
53-
name: 'Wallet 2',
54-
},
55-
groups: {
56-
'01JKAF3PJ247KAM6C03G5Q0NP8:default': {
57-
id: '01JKAF3PJ247KAM6C03G5Q0NP8:default',
58-
metadata: {
59-
name: 'Account 1 from wallet 2',
60-
},
61-
accounts: ['784225f4-d30b-4e77-a900-c8bbce735b88'],
62-
},
63-
},
64-
},
65-
},
66-
},
6733
},
6834
});
6935

@@ -76,15 +42,13 @@ describe('AccountList', () => {
7642
expect(screen.getByText('Accounts')).toBeInTheDocument();
7743
expect(screen.getByLabelText('Back')).toBeInTheDocument();
7844

79-
const walletHeaders = screen.getAllByTestId(
80-
'multichain-account-tree-wallet-header',
81-
);
45+
const walletHeaders = screen.getAllByTestId(walletHeaderTestId);
8246

83-
expect(walletHeaders.length).toBe(2);
47+
expect(walletHeaders.length).toBe(5);
8448
expect(screen.getByText('Wallet 1')).toBeInTheDocument();
8549
expect(screen.getByText('Wallet 2')).toBeInTheDocument();
86-
expect(screen.getByText('Account 1 from wallet 1')).toBeInTheDocument();
87-
expect(screen.getByText('Account 1 from wallet 2')).toBeInTheDocument();
50+
expect(screen.getByText('Account 1')).toBeInTheDocument();
51+
expect(screen.getByText('Account 2')).toBeInTheDocument();
8852
});
8953

9054
it('calls history.goBack when back button is clicked', () => {
@@ -110,4 +74,85 @@ describe('AccountList', () => {
11074
expect(screen.getByText('Import an account')).toBeInTheDocument();
11175
expect(screen.getByText('Add a hardware wallet')).toBeInTheDocument();
11276
});
77+
78+
it('displays the search field with correct placeholder', () => {
79+
renderComponent();
80+
81+
const searchContainer = screen.getByTestId(searchContainerTestId);
82+
83+
expect(searchContainer).toBeInTheDocument();
84+
85+
const searchInput = within(searchContainer).getByPlaceholderText(
86+
'Search your accounts',
87+
);
88+
89+
expect(searchInput).toBeInTheDocument();
90+
});
91+
92+
it('updates search value when typing in the search field', () => {
93+
renderComponent();
94+
95+
const searchContainer = screen.getByTestId(searchContainerTestId);
96+
const searchInput = within(searchContainer).getByRole('searchbox');
97+
fireEvent.change(searchInput, { target: { value: 'Account 2' } });
98+
99+
// @ts-expect-error Values does exist on the search input
100+
expect(searchInput?.value).toBe('Account 2');
101+
});
102+
103+
it('filters accounts when search text is entered', () => {
104+
renderComponent();
105+
106+
// Verify all accounts are shown initially
107+
const walletHeaders = screen.getAllByTestId(walletHeaderTestId);
108+
expect(walletHeaders.length).toBe(5);
109+
expect(screen.getByText('Account 1')).toBeInTheDocument();
110+
expect(screen.getByText('Account 2')).toBeInTheDocument();
111+
112+
const searchContainer = screen.getByTestId(searchContainerTestId);
113+
const searchInput = within(searchContainer).getByRole('searchbox');
114+
fireEvent.change(searchInput, { target: { value: 'Account 2' } });
115+
116+
expect(screen.queryByText('Account 1')).not.toBeInTheDocument();
117+
expect(screen.getByText('Account 2')).toBeInTheDocument();
118+
});
119+
120+
it('shows "No accounts found" message when no accounts match search criteria', () => {
121+
renderComponent();
122+
123+
const searchContainer = screen.getByTestId(searchContainerTestId);
124+
const searchInput = within(searchContainer).getByRole('searchbox');
125+
fireEvent.change(searchInput, { target: { value: 'nonexistent account' } });
126+
127+
expect(
128+
screen.getByText('No accounts found for the given search query'),
129+
).toBeInTheDocument();
130+
});
131+
132+
it('clears search when clear button is clicked', () => {
133+
renderComponent();
134+
135+
const searchContainer = screen.getByTestId(searchContainerTestId);
136+
const searchInput = within(searchContainer).getByRole('searchbox');
137+
fireEvent.change(searchInput, { target: { value: 'Account 2' } });
138+
139+
const clearButton = screen.getByTestId(searchClearButtonTestId);
140+
fireEvent.click(clearButton);
141+
142+
// @ts-expect-error Value does exist on search input
143+
expect(searchInput?.value).toBe('');
144+
expect(screen.getByText('Account 1')).toBeInTheDocument();
145+
expect(screen.getByText('Account 2')).toBeInTheDocument();
146+
});
147+
148+
it('performs case-insensitive search', () => {
149+
renderComponent();
150+
151+
const searchContainer = screen.getByTestId(searchContainerTestId);
152+
const searchInput = within(searchContainer).getByRole('searchbox');
153+
fireEvent.change(searchInput, { target: { value: 'account 2' } });
154+
155+
expect(screen.queryByText('Account 1')).not.toBeInTheDocument();
156+
expect(screen.getByText('Account 2')).toBeInTheDocument();
157+
});
113158
});

0 commit comments

Comments
 (0)