Skip to content

Commit 3a22704

Browse files
authored
feat: Add AddressInput component (#14571)
## **Description** Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? Porting `AddressInput` component from the extension. ## **Related issues** Fixes: #14329 ## **Manual testing steps** 1. Pull down the branch and run in the simulator 2. Navigate to the test-snaps page and trigger the custom UI from the send flow snap 3. Observe the results. ## **Screenshots/Recordings** ### **After** **Normal scenario** https://github.com/user-attachments/assets/ac2199af-41c7-4a51-9e2e-66866252041c **Matched, Disabled & Error** https://github.com/user-attachments/assets/45ba0394-ddae-4c60-9d11-9a066de0f971 **Matched, Disabled** https://github.com/user-attachments/assets/ed9e30ba-3767-4049-88e1-2c701d16d74f ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/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-mobile/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 68d4900 commit 3a22704

File tree

8 files changed

+911
-0
lines changed

8 files changed

+911
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import React from 'react';
2+
import { fireEvent, render } from '@testing-library/react-native';
3+
import { SnapUIAddressInput } from './SnapUIAddressInput';
4+
import { useSnapInterfaceContext } from '../SnapInterfaceContext';
5+
import { useDisplayName } from '../SnapUIAddress/useDisplayName';
6+
import { INPUT_TEST_ID } from '../../../component-library/components/Form/TextField/foundation/Input/Input.constants';
7+
import renderWithProvider from '../../../util/test/renderWithProvider';
8+
9+
const mockInitialState = {
10+
settings: {
11+
useBlockieIcon: false,
12+
},
13+
};
14+
15+
jest.mock('../SnapInterfaceContext');
16+
jest.mock('../SnapUIAddress/useDisplayName');
17+
18+
describe('SnapUIAddressInput', () => {
19+
const mockHandleInputChange = jest.fn();
20+
const mockSetCurrentFocusedInput = jest.fn();
21+
const mockGetValue = jest.fn();
22+
23+
// Define a valid Ethereum chain ID for testing
24+
const testChainId = `eip155:0`;
25+
const testAddress = '0xabcd5d886577d5081b0c52e242ef29e70be3e7bc';
26+
27+
beforeEach(() => {
28+
(useSnapInterfaceContext as jest.Mock).mockReturnValue({
29+
handleInputChange: mockHandleInputChange,
30+
getValue: mockGetValue,
31+
setCurrentFocusedInput: mockSetCurrentFocusedInput,
32+
focusedInput: null,
33+
});
34+
35+
(useDisplayName as jest.Mock).mockReturnValue(undefined);
36+
37+
jest.clearAllMocks();
38+
});
39+
40+
it('will render', () => {
41+
const { getByTestId } = render(
42+
<SnapUIAddressInput name="testAddress" chainId={testChainId} />,
43+
);
44+
45+
expect(getByTestId(INPUT_TEST_ID)).toBeTruthy();
46+
});
47+
48+
it('supports existing state', () => {
49+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
50+
51+
const { getByDisplayValue } = renderWithProvider(
52+
<SnapUIAddressInput name="testAddress" chainId={testChainId} />,
53+
{ state: mockInitialState },
54+
);
55+
56+
expect(getByDisplayValue(testAddress)).toBeTruthy();
57+
});
58+
59+
it('can accept input', () => {
60+
const { getByTestId } = renderWithProvider(
61+
<SnapUIAddressInput name="testAddress" chainId={testChainId} />,
62+
{ state: mockInitialState },
63+
);
64+
65+
const textfield = getByTestId(INPUT_TEST_ID);
66+
fireEvent.changeText(textfield, '0x');
67+
expect(mockHandleInputChange).toHaveBeenCalledWith(
68+
'testAddress',
69+
`${testChainId}:0x`,
70+
undefined,
71+
);
72+
});
73+
74+
it('supports a placeholder', () => {
75+
const placeholder = 'Enter ETH address';
76+
const { getByTestId } = renderWithProvider(
77+
<SnapUIAddressInput
78+
name="testAddress"
79+
chainId={testChainId}
80+
placeholder={placeholder}
81+
/>,
82+
{ state: mockInitialState },
83+
);
84+
85+
const textfield = getByTestId(INPUT_TEST_ID);
86+
expect(textfield.props.placeholder).toBe(placeholder);
87+
});
88+
89+
it('supports the disabled prop', () => {
90+
const { getByTestId } = renderWithProvider(
91+
<SnapUIAddressInput
92+
name="testAddress"
93+
chainId={testChainId}
94+
disabled
95+
/>,
96+
{ state: mockInitialState },
97+
);
98+
99+
const textfield = getByTestId(INPUT_TEST_ID);
100+
expect(textfield.props.editable).toBe(false);
101+
});
102+
103+
it('will render within a field', () => {
104+
const label = 'Address Field';
105+
const { getByText } = renderWithProvider(
106+
<SnapUIAddressInput
107+
name="testAddress"
108+
chainId={testChainId}
109+
label={label}
110+
/>,
111+
{ state: mockInitialState },
112+
);
113+
114+
expect(getByText(label)).toBeTruthy();
115+
});
116+
117+
it('will render a matched address name', () => {
118+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
119+
const displayName = 'Vitalik.eth';
120+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
121+
122+
const { getByText } = renderWithProvider(
123+
<SnapUIAddressInput name="testAddress" chainId={testChainId} />,
124+
{ state: mockInitialState },
125+
);
126+
127+
expect(getByText(displayName)).toBeTruthy();
128+
expect(getByText(testAddress)).toBeTruthy();
129+
});
130+
131+
it('will render avatar when displayAvatar is true', () => {
132+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
133+
const displayName = 'Vitalik.eth';
134+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
135+
136+
const { toJSON } = renderWithProvider(
137+
<SnapUIAddressInput
138+
name="testAddress"
139+
chainId={testChainId}
140+
displayAvatar
141+
/>,
142+
{ state: mockInitialState },
143+
);
144+
145+
146+
const tree = JSON.stringify(toJSON());
147+
expect(tree.includes('RNSVGSvgView')).toBe(true);
148+
});
149+
150+
it('will not render avatar when displayAvatar is false', () => {
151+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
152+
const displayName = 'Vitalik.eth';
153+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
154+
155+
const { toJSON } = renderWithProvider(
156+
<SnapUIAddressInput
157+
name="testAddress"
158+
chainId={testChainId}
159+
displayAvatar={false}
160+
/>,
161+
{ state: mockInitialState },
162+
);
163+
164+
const tree = JSON.stringify(toJSON());
165+
expect(tree.includes('RNSVGSvgView')).toBe(false);
166+
});
167+
168+
it('renders with an invalid CAIP Account ID', () => {
169+
mockGetValue.mockReturnValue('eip155:0:https://foobar.baz/foobar');
170+
171+
const { toJSON } = renderWithProvider(<SnapUIAddressInput name="input" chainId="eip155:0" displayAvatar={false} />);
172+
173+
expect(toJSON()).toMatchSnapshot();
174+
});
175+
176+
it('renders the matched address info in a disabled state', () => {
177+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
178+
const displayName = 'Vitalik.eth';
179+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
180+
181+
const { getByText, toJSON, getByTestId } = renderWithProvider(
182+
<SnapUIAddressInput name="testAddress" chainId={testChainId} disabled />,
183+
{ state: mockInitialState },
184+
);
185+
186+
expect(getByText(displayName)).toBeTruthy();
187+
expect(getByText(testAddress)).toBeTruthy();
188+
const closeButton = getByTestId('snap-ui-address-input__clear-button');
189+
expect(closeButton).toBeTruthy();
190+
191+
const tree = JSON.stringify(toJSON());
192+
193+
expect(tree).toContain('"opacity":0.5');
194+
expect(tree).toContain('"disabled":true');
195+
expect(toJSON()).toMatchSnapshot();
196+
});
197+
198+
it('does not render the matched address info if there is an error', () => {
199+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
200+
const displayName = 'Vitalik.eth';
201+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
202+
203+
const { queryByText, queryByTestId, getByText, toJSON } = renderWithProvider(
204+
<SnapUIAddressInput name="testAddress" chainId={testChainId} error="Error" />,
205+
{ state: mockInitialState },
206+
);
207+
208+
expect(queryByText(displayName)).toBeFalsy();
209+
210+
const input = queryByTestId(INPUT_TEST_ID);
211+
212+
expect(input.props.value).toBe(testAddress);
213+
expect(getByText('Error')).toBeTruthy();
214+
215+
const tree = JSON.stringify(toJSON());
216+
217+
expect(tree.includes('RNSVGSvgView')).toBe(true);
218+
});
219+
220+
it('disables clear button for the input when disabled', () => {
221+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
222+
223+
const { getByTestId, toJSON } = renderWithProvider(
224+
<SnapUIAddressInput name="testAddress" chainId={testChainId} disabled />,
225+
);
226+
227+
const input = getByTestId(INPUT_TEST_ID);
228+
expect(input.props.editable).toBe(false);
229+
expect(input.props.value).toBe(testAddress);
230+
231+
const closeButton = getByTestId('snap-ui-address-input__clear-button');
232+
expect(closeButton).toBeTruthy();
233+
234+
const tree = JSON.stringify(toJSON());
235+
expect(tree).toContain('"disabled":true');
236+
});
237+
238+
it('disables clear button for the matched address info when disabled', () => {
239+
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
240+
const displayName = 'Vitalik.eth';
241+
(useDisplayName as jest.Mock).mockReturnValue(displayName);
242+
243+
const { getByText, getByTestId, toJSON } = renderWithProvider(
244+
<SnapUIAddressInput name="testAddress" chainId={testChainId} disabled />,
245+
{ state: mockInitialState },
246+
);
247+
248+
expect(getByText(displayName)).toBeTruthy();
249+
expect(getByText(testAddress)).toBeTruthy();
250+
251+
const closeButton = getByTestId('snap-ui-address-input__clear-button');
252+
expect(closeButton).toBeTruthy();
253+
254+
const tree = JSON.stringify(toJSON());
255+
expect(tree).toContain('"disabled":true');
256+
});
257+
});

0 commit comments

Comments
 (0)