Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAKING] Support Interactive UI in snaps-jest #2286

Merged
merged 9 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved

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();
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved

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();
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved

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
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
Loading