Skip to content

Commit 51a1d04

Browse files
authored
[BREAKING] Support Interactive UI in snaps-jest (#2286)
This adds new methods to support user interactions in `snaps-jest`. - Add `clickElement` method to allow a click simulation on an element. - Add `typeInField` method to allow field typing simulation. - Add those new methods to `getInteface` result for `snap_dialog`. - [BREAKING] Refactor the snap handler result object to remove the static `content` field and replace it with `getInterface`, this allows to get the interface after an update due to a user interaction.
1 parent de7fc0e commit 51a1d04

File tree

23 files changed

+1464
-161
lines changed

23 files changed

+1464
-161
lines changed

packages/examples/packages/home-page/src/index.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ describe('onHomePage', () => {
88

99
const response = await onHomePage();
1010

11-
expect(response).toRender(
11+
const screen = response.getInterface();
12+
13+
expect(screen).toRender(
1214
panel([heading('Hello world!'), text('Welcome to my Snap home page!')]),
1315
);
1416
});

packages/examples/packages/interactive-ui/src/index.test.ts

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import { expect } from '@jest/globals';
22
import { installSnap } from '@metamask/snaps-jest';
3-
import { address, button, heading, panel, row } from '@metamask/snaps-sdk';
3+
import {
4+
ButtonType,
5+
address,
6+
button,
7+
copyable,
8+
form,
9+
heading,
10+
input,
11+
panel,
12+
row,
13+
text,
14+
} from '@metamask/snaps-sdk';
415
import { assert } from '@metamask/utils';
516

617
describe('onRpcRequest', () => {
@@ -30,17 +41,50 @@ describe('onRpcRequest', () => {
3041
method: 'dialog',
3142
});
3243

33-
const ui = await response.getInterface();
34-
assert(ui.type === 'confirmation');
44+
const startScreen = await response.getInterface();
45+
assert(startScreen.type === 'confirmation');
3546

36-
expect(ui).toRender(
47+
expect(startScreen).toRender(
3748
panel([
3849
heading('Interactive UI Example Snap'),
3950
button({ value: 'Update UI', name: 'update' }),
4051
]),
4152
);
4253

43-
await ui.ok();
54+
await startScreen.clickElement('update');
55+
56+
const formScreen = await response.getInterface();
57+
58+
expect(formScreen).toRender(
59+
panel([
60+
heading('Interactive UI Example Snap'),
61+
form({
62+
name: 'example-form',
63+
children: [
64+
input({
65+
name: 'example-input',
66+
placeholder: 'Enter something...',
67+
}),
68+
button('Submit', ButtonType.Submit, 'submit'),
69+
],
70+
}),
71+
]),
72+
);
73+
74+
await formScreen.typeInField('example-input', 'foobar');
75+
76+
await formScreen.clickElement('submit');
77+
78+
const resultScreen = await response.getInterface();
79+
80+
expect(resultScreen).toRender(
81+
panel([
82+
heading('Interactive UI Example Snap'),
83+
text('The submitted value is:'),
84+
copyable('foobar'),
85+
]),
86+
);
87+
await resultScreen.ok();
4488

4589
expect(await response).toRespondWith(true);
4690
});
@@ -73,12 +117,48 @@ describe('onHomePage', () => {
73117

74118
const response = await onHomePage();
75119

76-
expect(response).toRender(
120+
const startScreen = response.getInterface();
121+
122+
expect(startScreen).toRender(
77123
panel([
78124
heading('Interactive UI Example Snap'),
79125
button({ value: 'Update UI', name: 'update' }),
80126
]),
81127
);
128+
129+
await startScreen.clickElement('update');
130+
131+
const formScreen = response.getInterface();
132+
133+
expect(formScreen).toRender(
134+
panel([
135+
heading('Interactive UI Example Snap'),
136+
form({
137+
name: 'example-form',
138+
children: [
139+
input({
140+
name: 'example-input',
141+
placeholder: 'Enter something...',
142+
}),
143+
button('Submit', ButtonType.Submit, 'submit'),
144+
],
145+
}),
146+
]),
147+
);
148+
149+
await formScreen.typeInField('example-input', 'foobar');
150+
151+
await formScreen.clickElement('submit');
152+
153+
const resultScreen = response.getInterface();
154+
155+
expect(resultScreen).toRender(
156+
panel([
157+
heading('Interactive UI Example Snap'),
158+
text('The submitted value is:'),
159+
copyable('foobar'),
160+
]),
161+
);
82162
});
83163
});
84164

@@ -96,12 +176,25 @@ describe('onTransaction', () => {
96176
data: '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
97177
});
98178

99-
expect(response).toRender(
179+
const startScreen = response.getInterface();
180+
181+
expect(startScreen).toRender(
100182
panel([
101183
row('From', address(FROM_ADDRESS)),
102184
row('To', address(TO_ADDRESS)),
103185
button({ value: 'See transaction type', name: 'transaction-type' }),
104186
]),
105187
);
188+
189+
await startScreen.clickElement('transaction-type');
190+
191+
const txTypeScreen = response.getInterface();
192+
193+
expect(txTypeScreen).toRender(
194+
panel([
195+
row('Transaction type', text('ERC-20')),
196+
button({ value: 'Go back', name: 'go-back' }),
197+
]),
198+
);
106199
});
107200
});

packages/examples/packages/signature-insights/src/index.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ describe('onSignature', () => {
1313
data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0',
1414
});
1515

16-
expect(response).toRender(
16+
const screen = response.getInterface();
17+
18+
expect(screen).toRender(
1719
panel([
1820
row('From:', text('0xd8da6bf26964af9d7eed9e03e53415d37aa96045')),
1921
row(

packages/examples/packages/transaction-insights/src/index.test.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ describe('onTransaction', () => {
1717
data: '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
1818
});
1919

20-
expect(response).toRender(
20+
const screen = response.getInterface();
21+
22+
expect(screen).toRender(
2123
panel([
2224
row('From', address(FROM_ADDRESS)),
2325
row('To', address(TO_ADDRESS)),
@@ -37,7 +39,9 @@ describe('onTransaction', () => {
3739
data: '0x23b872dd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3840
});
3941

40-
expect(response).toRender(
42+
const screen = response.getInterface();
43+
44+
expect(screen).toRender(
4145
panel([
4246
row('From', address(FROM_ADDRESS)),
4347
row('To', address(TO_ADDRESS)),
@@ -57,7 +61,9 @@ describe('onTransaction', () => {
5761
data: '0xf242432a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
5862
});
5963

60-
expect(response).toRender(
64+
const screen = response.getInterface();
65+
66+
expect(screen).toRender(
6167
panel([
6268
row('From', address(FROM_ADDRESS)),
6369
row('To', address(TO_ADDRESS)),
@@ -75,7 +81,9 @@ describe('onTransaction', () => {
7581
data: '0xabcdef1200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
7682
});
7783

78-
expect(response).toRender(
84+
const screen = response.getInterface();
85+
86+
expect(screen).toRender(
7987
panel([
8088
row('From', address(FROM_ADDRESS)),
8189
row('To', address(TO_ADDRESS)),
@@ -93,7 +101,9 @@ describe('onTransaction', () => {
93101
data: '0x',
94102
});
95103

96-
expect(response).toRender(
104+
const screen = response.getInterface();
105+
106+
expect(screen).toRender(
97107
panel([
98108
row('From', address(FROM_ADDRESS)),
99109
row('To', address(TO_ADDRESS)),

packages/snaps-jest/README.md

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ All properties are optional, and have sensible defaults. The addresses are
194194
randomly generated by default. Most values can be specified as a hex string, or
195195
a decimal number.
196196

197-
It returns an object with the user interface that was shown by the snap, in the
197+
It returns a `getInterface` function that gets the user interface that was shown by the snap, in the
198198
[onTransaction](https://docs.metamask.io/snaps/reference/exports/#ontransaction)
199199
function.
200200

@@ -214,7 +214,9 @@ describe('MySnap', () => {
214214
nonce: '0x0',
215215
});
216216

217-
expect(response).toRender(panel([text('Hello, world!')]));
217+
const screen = response.getInterface();
218+
219+
expect(screen).toRender(panel([text('Hello, world!')]));
218220
});
219221
});
220222
```
@@ -233,7 +235,7 @@ All properties are optional, and have sensible defaults. The addresses are
233235
randomly generated by default. Most values can be specified as a hex string, or
234236
a decimal number.
235237

236-
It returns an object with the user interface that was shown by the snap, in the
238+
It returns a `getInterface` function that gets the user interface that was shown by the snap, in the
237239
[onSignature](https://docs.metamask.io/snaps/reference/exports/#onsignature)
238240
function.
239241

@@ -246,7 +248,9 @@ describe('MySnap', () => {
246248
const { onSignature } = await installSnap(/* optional snap ID */);
247249
const response = await onSignature();
248250

249-
expect(response).toRender(
251+
const screen = response.getInterface();
252+
253+
expect(screen).toRender(
250254
panel([text('You are using the personal_sign method')]),
251255
);
252256
});
@@ -303,7 +307,7 @@ describe('MySnap', () => {
303307
### `snap.onHomePage`
304308

305309
The `onHomePage` function can be used to request the home page of the snap. It
306-
takes no arguments, and returns a promise that resolves to the response from the
310+
takes no arguments, and returns a promise that contains a `getInterface` function to get the response from the
307311
[onHomePage](https://docs.metamask.io/snaps/reference/entry-points/#onhomepage)
308312
function.
309313

@@ -318,7 +322,9 @@ describe('MySnap', () => {
318322
params: [],
319323
});
320324

321-
expect(response).toRender(/* ... */);
325+
const screen = response.getInterface();
326+
327+
expect(screen).toRender(/* ... */);
322328
});
323329
});
324330
```
@@ -344,14 +350,16 @@ assert that a response from a snap matches an expected value:
344350

345351
### Interacting with user interfaces
346352

353+
#### `snap_dialog`
354+
347355
If your snap uses `snap_dialog` to show user interfaces, you can use the
348356
`request.getInterface` function to interact with them. This method is present on
349357
the return value of the `snap.request` function.
350358

351359
It waits for the user interface to be shown, and returns an object with
352360
functions that can be used to interact with the user interface.
353361

354-
#### Example
362+
##### Example
355363

356364
```js
357365
import { installSnap } from '@metamask/snaps-jest';
@@ -384,6 +392,91 @@ describe('MySnap', () => {
384392
});
385393
```
386394

395+
#### handlers
396+
397+
If your snap uses handlers that shows user interfaces (`onTransaction`, `onSignature`, `onHomePage`), you can use the
398+
`response.getInterface` function to interact with them. This method is present on
399+
the return value of the `snap.request` function.
400+
401+
It returns an object with functions that can be used to interact with the user interface.
402+
403+
##### Example
404+
405+
```js
406+
import { installSnap } from '@metamask/snaps-jest';
407+
408+
describe('MySnap', () => {
409+
it('should do something', async () => {
410+
const { onHomePage } = await installSnap(/* optional snap ID */);
411+
const response = await onHomePage({
412+
method: 'foo',
413+
params: [],
414+
});
415+
416+
const screen = response.getInterface();
417+
418+
expect(screen).toRender(/* ... */);
419+
});
420+
});
421+
```
422+
423+
### User interactions in user interfaces
424+
425+
The object returned by the `getInterface` function exposes other functions to trigger user interactions in the user interface.
426+
427+
- `clickElement(elementName)`: Click on a button inside the user interface. If the button with the given name does not exist in the interface this method will throw.
428+
- `typeInField(elementName, valueToType)`: Enter a value in a field inside the user interface. If the input field with the given name des not exist in the interface this method will throw.
429+
430+
#### Example
431+
432+
```js
433+
import { installSnap } from '@metamask/snaps-jest';
434+
435+
describe('MySnap', () => {
436+
it('should do something', async () => {
437+
const { onHomePage } = await installSnap(/* optional snap ID */);
438+
const response = await onHomePage({
439+
method: 'foo',
440+
params: [],
441+
});
442+
443+
const screen = response.getInterface();
444+
445+
expect(screen).toRender(/* ... */);
446+
447+
await screen.clickElement('myButton');
448+
449+
const screen = response.getInterface();
450+
451+
expect(screen).toRender(/* ... */);
452+
});
453+
});
454+
```
455+
456+
```js
457+
import { installSnap } from '@metamask/snaps-jest';
458+
459+
describe('MySnap', () => {
460+
it('should do something', async () => {
461+
const { onHomePage } = await installSnap(/* optional snap ID */);
462+
const response = await onHomePage({
463+
method: 'foo',
464+
params: [],
465+
});
466+
467+
const screen = response.getInterface();
468+
469+
expect(screen).toRender(/* ... */);
470+
471+
await screen.typeInField('myField', 'the value to type');
472+
473+
const screen = response.getInterface();
474+
475+
expect(screen).toRender(/* ... */);
476+
});
477+
});
478+
```
479+
387480
## Options
388481

389482
You can pass options to the test environment by adding a

0 commit comments

Comments
 (0)