Skip to content

Commit

Permalink
[BREAKING] Support Interactive UI in snaps-jest (#2286)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
GuillaumeRx authored Mar 27, 2024
1 parent de7fc0e commit 51a1d04
Show file tree
Hide file tree
Showing 23 changed files with 1,464 additions and 161 deletions.
4 changes: 3 additions & 1 deletion packages/examples/packages/home-page/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ describe('onHomePage', () => {

const response = await onHomePage();

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([heading('Hello world!'), text('Welcome to my Snap home page!')]),
);
});
Expand Down
107 changes: 100 additions & 7 deletions packages/examples/packages/interactive-ui/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { expect } from '@jest/globals';
import { installSnap } from '@metamask/snaps-jest';
import { address, button, heading, panel, row } from '@metamask/snaps-sdk';
import {
ButtonType,
address,
button,
copyable,
form,
heading,
input,
panel,
row,
text,
} from '@metamask/snaps-sdk';
import { assert } from '@metamask/utils';

describe('onRpcRequest', () => {
Expand Down Expand Up @@ -30,17 +41,50 @@ describe('onRpcRequest', () => {
method: 'dialog',
});

const ui = await response.getInterface();
assert(ui.type === 'confirmation');
const startScreen = await response.getInterface();
assert(startScreen.type === 'confirmation');

expect(ui).toRender(
expect(startScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
button({ value: 'Update UI', name: 'update' }),
]),
);

await ui.ok();
await startScreen.clickElement('update');

const formScreen = await response.getInterface();

expect(formScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
form({
name: 'example-form',
children: [
input({
name: 'example-input',
placeholder: 'Enter something...',
}),
button('Submit', ButtonType.Submit, 'submit'),
],
}),
]),
);

await formScreen.typeInField('example-input', 'foobar');

await formScreen.clickElement('submit');

const resultScreen = await response.getInterface();

expect(resultScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
text('The submitted value is:'),
copyable('foobar'),
]),
);
await resultScreen.ok();

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

const response = await onHomePage();

expect(response).toRender(
const startScreen = response.getInterface();

expect(startScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
button({ value: 'Update UI', name: 'update' }),
]),
);

await startScreen.clickElement('update');

const formScreen = response.getInterface();

expect(formScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
form({
name: 'example-form',
children: [
input({
name: 'example-input',
placeholder: 'Enter something...',
}),
button('Submit', ButtonType.Submit, 'submit'),
],
}),
]),
);

await formScreen.typeInField('example-input', 'foobar');

await formScreen.clickElement('submit');

const resultScreen = response.getInterface();

expect(resultScreen).toRender(
panel([
heading('Interactive UI Example Snap'),
text('The submitted value is:'),
copyable('foobar'),
]),
);
});
});

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

expect(response).toRender(
const startScreen = response.getInterface();

expect(startScreen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
button({ value: 'See transaction type', name: 'transaction-type' }),
]),
);

await startScreen.clickElement('transaction-type');

const txTypeScreen = response.getInterface();

expect(txTypeScreen).toRender(
panel([
row('Transaction type', text('ERC-20')),
button({ value: 'Go back', name: 'go-back' }),
]),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe('onSignature', () => {
data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From:', text('0xd8da6bf26964af9d7eed9e03e53415d37aa96045')),
row(
Expand Down
20 changes: 15 additions & 5 deletions packages/examples/packages/transaction-insights/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('onTransaction', () => {
data: '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
Expand All @@ -37,7 +39,9 @@ describe('onTransaction', () => {
data: '0x23b872dd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
Expand All @@ -57,7 +61,9 @@ describe('onTransaction', () => {
data: '0xf242432a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
Expand All @@ -75,7 +81,9 @@ describe('onTransaction', () => {
data: '0xabcdef1200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
Expand All @@ -93,7 +101,9 @@ describe('onTransaction', () => {
data: '0x',
});

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([
row('From', address(FROM_ADDRESS)),
row('To', address(TO_ADDRESS)),
Expand Down
107 changes: 100 additions & 7 deletions packages/snaps-jest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ All properties are optional, and have sensible defaults. The addresses are
randomly generated by default. Most values can be specified as a hex string, or
a decimal number.

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

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

expect(response).toRender(panel([text('Hello, world!')]));
const screen = response.getInterface();

expect(screen).toRender(panel([text('Hello, world!')]));
});
});
```
Expand All @@ -233,7 +235,7 @@ All properties are optional, and have sensible defaults. The addresses are
randomly generated by default. Most values can be specified as a hex string, or
a decimal number.

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

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

expect(response).toRender(
const screen = response.getInterface();

expect(screen).toRender(
panel([text('You are using the personal_sign method')]),
);
});
Expand Down Expand Up @@ -303,7 +307,7 @@ describe('MySnap', () => {
### `snap.onHomePage`

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

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

expect(response).toRender(/* ... */);
const screen = response.getInterface();

expect(screen).toRender(/* ... */);
});
});
```
Expand All @@ -344,14 +350,16 @@ assert that a response from a snap matches an expected value:

### Interacting with user interfaces

#### `snap_dialog`

If your snap uses `snap_dialog` to show user interfaces, you can use the
`request.getInterface` function to interact with them. This method is present on
the return value of the `snap.request` function.

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

#### Example
##### Example

```js
import { installSnap } from '@metamask/snaps-jest';
Expand Down Expand Up @@ -384,6 +392,91 @@ describe('MySnap', () => {
});
```

#### handlers

If your snap uses handlers that shows user interfaces (`onTransaction`, `onSignature`, `onHomePage`), you can use the
`response.getInterface` function to interact with them. This method is present on
the return value of the `snap.request` function.

It returns an object with functions that can be used to interact with the user interface.

##### Example

```js
import { installSnap } from '@metamask/snaps-jest';

describe('MySnap', () => {
it('should do something', async () => {
const { onHomePage } = await installSnap(/* optional snap ID */);
const response = await onHomePage({
method: 'foo',
params: [],
});

const screen = response.getInterface();

expect(screen).toRender(/* ... */);
});
});
```

### User interactions in user interfaces

The object returned by the `getInterface` function exposes other functions to trigger user interactions in the user interface.

- `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.
- `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.

#### Example

```js
import { installSnap } from '@metamask/snaps-jest';

describe('MySnap', () => {
it('should do something', async () => {
const { onHomePage } = await installSnap(/* optional snap ID */);
const response = await onHomePage({
method: 'foo',
params: [],
});

const screen = response.getInterface();

expect(screen).toRender(/* ... */);

await screen.clickElement('myButton');

const screen = response.getInterface();

expect(screen).toRender(/* ... */);
});
});
```

```js
import { installSnap } from '@metamask/snaps-jest';

describe('MySnap', () => {
it('should do something', async () => {
const { onHomePage } = await installSnap(/* optional snap ID */);
const response = await onHomePage({
method: 'foo',
params: [],
});

const screen = response.getInterface();

expect(screen).toRender(/* ... */);

await screen.typeInField('myField', 'the value to type');

const screen = response.getInterface();

expect(screen).toRender(/* ... */);
});
});
```

## Options

You can pass options to the test environment by adding a
Expand Down
Loading

0 comments on commit 51a1d04

Please sign in to comment.