Skip to content

Commit 8589b4e

Browse files
authored
fix(send-flow-snap): add missing displayAvatar props, add tests, update snaps-simulation (#3312)
The send-flow example snap is only passing the `displayAvatar` prop in the `to` scenario but not in the others. This can land you in a situation where you paste a valid address in the to field, start to type an amount in the amount field, then the avatar disappears because no `displayAvatar` prop is being passed. ### Summary of changes - Updated the send flow snap's `onUserInput` code to properly pass the `displayAvatar` prop in all scenarios. - Added tests for the snap. - Added an SVG transformer so jest can handle the icons that are imported in the snap. - Updated the structure of the SVGs so it's consistent with what jest expects. - Updated `typeInField` in `snaps-simulation` so that it properly attaches the `chainId` to the input value.
1 parent d12606d commit 8589b4e

File tree

13 files changed

+384
-43
lines changed

13 files changed

+384
-43
lines changed

packages/examples/packages/send-flow/jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ module.exports = deepmerge(baseConfig, {
3333
'<rootDir>/node_modules/@metamask/$1',
3434
],
3535
},
36+
37+
// Transform SVG files using our custom transformer
38+
transform: {
39+
...baseConfig.transform,
40+
'\\.svg$': '<rootDir>/test/transformers/svgTransformer.js',
41+
},
3642
});

packages/examples/packages/send-flow/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "u6ivnu9fXa7saZ10sruh8I3ZKZjeAur4W+9+LVp9BoU=",
10+
"shasum": "FOyn/lPGc/Fu6cW6EgX8OwklVJ/+HBydyqz+IDE8M+U=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
Lines changed: 1 addition & 12 deletions
Loading

packages/examples/packages/send-flow/src/images/jazzicon1.svg

Lines changed: 1 addition & 11 deletions
Loading

packages/examples/packages/send-flow/src/images/jazzicon2.svg

Lines changed: 1 addition & 11 deletions
Loading

packages/examples/packages/send-flow/src/index.test.tsx

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { expect } from '@jest/globals';
22
import { installSnap } from '@metamask/snaps-jest';
33

4+
import { SendFlow } from './components';
5+
import { accountsArray } from './data';
6+
47
describe('onRpcRequest', () => {
58
it('throws an error if the requested method does not exist', async () => {
69
const { request } = await installSnap();
@@ -21,6 +24,222 @@ describe('onRpcRequest', () => {
2124
});
2225

2326
describe('display', () => {
24-
it.todo('shows a custom dialog with the SendFlow interface');
27+
it('shows a custom dialog with the SendFlow interface', async () => {
28+
const { request } = await installSnap();
29+
30+
const response = request({
31+
method: 'display',
32+
});
33+
34+
const sendFlowInterface = await response.getInterface();
35+
36+
expect(sendFlowInterface).toRender(
37+
<SendFlow
38+
accounts={accountsArray}
39+
selectedAccount={accountsArray[0].address}
40+
selectedCurrency="BTC"
41+
total={{ amount: 0, fiat: 0 }}
42+
fees={{ amount: 1.0001, fiat: 1.23 }}
43+
/>,
44+
);
45+
});
46+
});
47+
});
48+
49+
describe('onHomePage', () => {
50+
it('returns a custom UI', async () => {
51+
const { onHomePage } = await installSnap();
52+
53+
const response = await onHomePage();
54+
55+
const sendFlowInterface = response.getInterface();
56+
57+
expect(sendFlowInterface).toRender(
58+
<SendFlow
59+
accounts={accountsArray}
60+
selectedAccount={accountsArray[0].address}
61+
selectedCurrency="BTC"
62+
total={{ amount: 0, fiat: 0 }}
63+
fees={{ amount: 1.0001, fiat: 1.23 }}
64+
/>,
65+
);
66+
});
67+
});
68+
69+
describe('onUserInput', () => {
70+
it('handles account selection', async () => {
71+
const { request } = await installSnap();
72+
73+
const response = request({
74+
method: 'display',
75+
});
76+
77+
const sendFlowInterface = await response.getInterface();
78+
79+
await sendFlowInterface.selectFromSelector(
80+
'accountSelector',
81+
accountsArray[1].address,
82+
);
83+
84+
const updatedInterface = await response.getInterface();
85+
86+
expect(updatedInterface).toRender(
87+
<SendFlow
88+
accounts={accountsArray}
89+
selectedAccount={accountsArray[1].address}
90+
selectedCurrency="BTC"
91+
total={{ amount: 1.0001, fiat: 251.23 }}
92+
fees={{ amount: 1.0001, fiat: 1.23 }}
93+
errors={{}}
94+
displayAvatar={false}
95+
/>,
96+
);
97+
});
98+
99+
it('handles amount input', async () => {
100+
const { request } = await installSnap();
101+
102+
const response = request({
103+
method: 'display',
104+
});
105+
106+
const sendFlowInterface = await response.getInterface();
107+
108+
await sendFlowInterface.typeInField('amount', '0.5');
109+
110+
const updatedInterface = await response.getInterface();
111+
112+
expect(updatedInterface).toRender(
113+
<SendFlow
114+
accounts={accountsArray}
115+
selectedAccount={accountsArray[0].address}
116+
selectedCurrency="BTC"
117+
total={{ amount: 1.5001, fiat: 251.23 }}
118+
fees={{ amount: 1.0001, fiat: 1.23 }}
119+
errors={{}}
120+
displayAvatar={false}
121+
/>,
122+
);
123+
});
124+
125+
it('handles to input', async () => {
126+
const { request } = await installSnap();
127+
128+
const response = request({
129+
method: 'display',
130+
});
131+
132+
const sendFlowInterface = await response.getInterface();
133+
134+
await sendFlowInterface.typeInField(
135+
'to',
136+
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
137+
);
138+
139+
const updatedInterface = await response.getInterface();
140+
141+
expect(updatedInterface).toRender(
142+
<SendFlow
143+
accounts={accountsArray}
144+
selectedAccount={accountsArray[0].address}
145+
selectedCurrency="BTC"
146+
total={{ amount: 1.0001, fiat: 251.23 }}
147+
fees={{ amount: 1.0001, fiat: 1.23 }}
148+
errors={{}}
149+
displayAvatar={true}
150+
/>,
151+
);
152+
});
153+
154+
it('handles invalid input', async () => {
155+
const { request } = await installSnap();
156+
157+
const response = request({
158+
method: 'display',
159+
});
160+
161+
const sendFlowInterface = await response.getInterface();
162+
163+
await sendFlowInterface.typeInField('amount', '2');
164+
165+
const updatedInterface = await response.getInterface();
166+
167+
expect(updatedInterface).toRender(
168+
<SendFlow
169+
accounts={accountsArray}
170+
selectedAccount={accountsArray[0].address}
171+
selectedCurrency="BTC"
172+
total={{ amount: 3.0000999999999998, fiat: 251.23 }}
173+
fees={{ amount: 1.0001, fiat: 1.23 }}
174+
errors={{ amount: 'Insufficient funds' }}
175+
displayAvatar={false}
176+
/>,
177+
);
178+
});
179+
180+
it('maintains state across multiple interactions', async () => {
181+
const { request } = await installSnap();
182+
183+
const response = request({
184+
method: 'display',
185+
});
186+
187+
const sendFlowInterface = await response.getInterface();
188+
189+
await sendFlowInterface.selectFromSelector(
190+
'accountSelector',
191+
accountsArray[1].address,
192+
);
193+
194+
await sendFlowInterface.typeInField('amount', '0.5');
195+
196+
const updatedInterface = await response.getInterface();
197+
198+
expect(updatedInterface).toRender(
199+
<SendFlow
200+
accounts={accountsArray}
201+
selectedAccount={accountsArray[1].address}
202+
selectedCurrency="BTC"
203+
total={{ amount: 1.5001, fiat: 251.23 }}
204+
fees={{ amount: 1.0001, fiat: 1.23 }}
205+
errors={{}}
206+
displayAvatar={false}
207+
/>,
208+
);
209+
210+
await sendFlowInterface.typeInField(
211+
'to',
212+
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
213+
);
214+
215+
const updatedInterface2 = await response.getInterface();
216+
217+
expect(updatedInterface2).toRender(
218+
<SendFlow
219+
accounts={accountsArray}
220+
selectedAccount={accountsArray[1].address}
221+
selectedCurrency="BTC"
222+
total={{ amount: 1.5001, fiat: 251.23 }}
223+
fees={{ amount: 1.0001, fiat: 1.23 }}
224+
errors={{}}
225+
displayAvatar={true}
226+
/>,
227+
);
228+
229+
await sendFlowInterface.typeInField('amount', '0');
230+
231+
const updatedInterface3 = await response.getInterface();
232+
233+
expect(updatedInterface3).toRender(
234+
<SendFlow
235+
accounts={accountsArray}
236+
selectedAccount={accountsArray[1].address}
237+
selectedCurrency="BTC"
238+
total={{ amount: 1.0001, fiat: 251.23 }}
239+
fees={{ amount: 1.0001, fiat: 1.23 }}
240+
errors={{}}
241+
displayAvatar={true}
242+
/>,
243+
);
25244
});
26245
});

packages/examples/packages/send-flow/src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ export const onUserInput: OnUserInputHandler = async ({
113113
errors={formErrors}
114114
// For testing purposes, we display the avatar if the address is
115115
// a valid hex checksum address.
116-
displayAvatar={isCaipHexAddress(event.value)}
116+
displayAvatar={isCaipHexAddress(
117+
event.name === 'to' ? event.value : sendForm.to,
118+
)}
117119
/>
118120
),
119121
},
@@ -134,6 +136,7 @@ export const onUserInput: OnUserInputHandler = async ({
134136
total={total}
135137
fees={fees}
136138
errors={formErrors}
139+
displayAvatar={isCaipHexAddress(sendForm.to)}
137140
/>
138141
),
139142
},
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* A custom transformer for SVG files in Jest tests.
3+
*/
4+
module.exports = {
5+
/**
6+
* Process an SVG file for Jest tests.
7+
*
8+
* @param {string} sourceText - The content of the SVG file.
9+
* @returns {object} The transformed code.
10+
*/
11+
process(sourceText) {
12+
return {
13+
code: `module.exports = ${JSON.stringify(sourceText)};`,
14+
};
15+
},
16+
17+
/**
18+
* Generate a cache key for the transformation.
19+
*
20+
* @param {string} sourceText - The content of the SVG file.
21+
* @returns {string} A cache key for Jest to use for caching purposes.
22+
*/
23+
getCacheKey(sourceText) {
24+
return sourceText;
25+
},
26+
};

packages/snaps-simulation/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ export const DEFAULT_CURRENCY = 'usd';
2626
* The default JSON-RPC endpoint for Ethereum requests.
2727
*/
2828
export const DEFAULT_JSON_RPC_ENDPOINT = 'https://cloudflare-eth.com/';
29+
30+
/**
31+
* The types of inputs that can be used in the `typeInField` interface action.
32+
*/
33+
export const TYPEABLE_INPUTS = ['Input', 'AddressInput'];

packages/snaps-simulation/src/interface.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
RadioGroup,
1919
Radio,
2020
Box,
21+
AddressInput,
2122
Input,
2223
FileInput,
2324
Checkbox,
@@ -924,6 +925,55 @@ describe('typeInField', () => {
924925
});
925926
});
926927

928+
it('updates the interface state and sends an InputChangeEvent for an AddressInput', async () => {
929+
jest.spyOn(rootControllerMessenger, 'call');
930+
const content = (
931+
<AddressInput
932+
name="addressInput"
933+
chainId="eip155:0"
934+
placeholder="Enter an address"
935+
/>
936+
);
937+
938+
const interfaceId = await interfaceController.createInterface(
939+
MOCK_SNAP_ID,
940+
content,
941+
);
942+
943+
await typeInField(
944+
rootControllerMessenger,
945+
interfaceId,
946+
content,
947+
MOCK_SNAP_ID,
948+
'addressInput',
949+
'0x1234567890123456789012345678901234567890',
950+
);
951+
952+
expect(rootControllerMessenger.call).toHaveBeenCalledWith(
953+
'SnapInterfaceController:updateInterfaceState',
954+
interfaceId,
955+
{ addressInput: 'eip155:0:0x1234567890123456789012345678901234567890' },
956+
);
957+
958+
expect(handleRpcRequestMock).toHaveBeenCalledWith(MOCK_SNAP_ID, {
959+
origin: 'metamask',
960+
handler: HandlerType.OnUserInput,
961+
request: {
962+
jsonrpc: '2.0',
963+
method: ' ',
964+
params: {
965+
event: {
966+
type: UserInputEventType.InputChangeEvent,
967+
name: 'addressInput',
968+
value: 'eip155:0:0x1234567890123456789012345678901234567890',
969+
},
970+
id: interfaceId,
971+
context: null,
972+
},
973+
},
974+
});
975+
});
976+
927977
it('throws if there is no inputs in the interface', async () => {
928978
const content = text('bar');
929979

@@ -964,7 +1014,7 @@ describe('typeInField', () => {
9641014
'baz',
9651015
),
9661016
).rejects.toThrow(
967-
'Expected an element of type "Input", but found "Button".',
1017+
'Expected an element of type "Input" or "AddressInput", but found "Button".',
9681018
);
9691019
});
9701020
});

0 commit comments

Comments
 (0)