UNPKG

@metamask/snaps-jest

Version:

A Jest preset for end-to-end testing MetaMask Snaps, including a Jest environment, and a set of Jest matchers

601 lines (458 loc) 17.1 kB
# `@metamask/snaps-jest` A [Jest](https://jestjs.io/) preset for end-to-end testing MetaMask Snaps, including a Jest environment, and a set of Jest matchers. - [Installation](#installation) - [Usage](#usage) - [Without the preset](#without-the-preset) - [API](#api) - [Install a snap](#install-a-snap) - [`snap.request`](#snaprequest) - [`snap.onTransaction`](#snapontransaction) - [`snap.onSignature`](#snaponsignature) - [`snap.onCronjob`](#snaponcronjob) - [`snap.onHomePage`](#snaponhomepage) - [Jest matchers](#jest-matchers) - [Interacting with user interfaces](#interacting-with-user-interfaces) - [Options](#options) ## Installation Use Node.js `16.0.0` or later. We recommend using [nvm](https://github.com/nvm-sh/nvm) for managing Node.js versions. Install a dependency in your snap project using `yarn` (or `npm`): - `yarn add -D @metamask/snaps-jest` ## Usage The easiest way to use this package is to add it to your Jest configuration as a preset. In your `jest.config.js` file, add the following: ```js module.exports = { preset: '@metamask/snaps-jest', }; ``` This will automatically configure Jest to use the `@metamask/snaps-jest` environment, and to use the `@metamask/snaps-jest` matchers. Then you can just run `jest` as usual. > **Note**: `@metamask/snaps-jest` assumes that the snap is built in the > directory you're running Jest from. If you're using a different directory, > you can specify the path to the snap using the [`root`](#options) option, or > by running your own HTTP server. > > Right now it's not possible to use `@metamask/snaps-jest` with a snap that > isn't built. ### Without the preset If you don't want to use the preset, you can still use the environment and matchers by adding them to your Jest configuration manually: ```js module.exports = { testEnvironment: '@metamask/snaps-jest', setupFilesAfterEnv: ['@metamask/snaps-jest/dist/cjs/setup.js'], }; ``` ## API ### Install a snap `@metamask/snaps-jest` exposes a `installSnap` function that can be used to install a snap in the execution environment. It's recommended to use this function in each test, to ensure that each test starts with a clean slate. By default, if the built-in server is enabled, it will install the snap from the built-in server. Otherwise, you must specify a snap ID to install. ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { await installSnap(/* optional snap ID */); // ... }); }); ``` The `installSnap` function returns an object with functions that can be used to interact with the snap. ### `snap.request` The `request` function can be used to send a JSON-RPC request to the snap. It takes a single argument, which is similar to a JSON-RPC request object, but with an additional optional `origin` property. It returns a promise that resolves to the response from the [onRpcRequest](https://docs.metamask.io/snaps/reference/exports/#onrpcrequest) function. ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { request } = await installSnap(/* optional snap ID */); const response = await request({ origin: 'http://localhost:8080', method: 'foo', params: [], }); // ... }); }); ``` It returns an object with a response, and some additional metadata, which can be checked using the [Jest matchers](#jest-matchers): ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { request } = await installSnap(/* optional snap ID */); const response = await request({ origin: 'http://localhost:8080', method: 'foo', params: [], }); expect(response).toRespondWith('bar'); expect(response).not.toRespondWithError('baz'); }); }); ``` #### Checking the response with standard Jest matchers If you prefer, you can also manually check the response: ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { request } = await installSnap(/* optional snap ID */); const { response } = await request({ origin: 'http://localhost:8080', method: 'foo', params: [], }); expect(response.result).toBe('bar'); expect(response.error).toBeUndefined(); }); }); ``` Since the response is a standard JSON-RPC response, you can use any Jest matchers to check it, including snapshot matchers: ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { request } = await installSnap(/* optional snap ID */); const { response } = await request({ origin: 'http://localhost:8080', method: 'foo', params: [], }); expect(response).toMatchSnapshot(); }); }); ``` ### `snap.onTransaction` The `onTransaction` function can be used to send a transaction to the snap. It takes a single argument, which is an object with the following properties: - `origin`: The origin of the transaction. - `chainId`: The chain ID of the transaction. - `from`: The address of the sender. - `to`: The address of the recipient. - `value`: The value of the transaction, in wei. - `data`: The data of the transaction. - `gasLimit`: The gas limit of the transaction. - `maxFeePerGas`: The maximum fee per gas of the transaction. - `maxPriorityFeePerGas`: The maximum priority fee per gas of the transaction. - `nonce`: The nonce of the transaction. 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 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. ```js import { installSnap } from '@metamask/snaps-jest'; import { panel, text } from '@metamask/snaps-sdk'; describe('MySnap', () => { it('should do something', async () => { const { onTransaction } = await installSnap(/* optional snap ID */); const response = await onTransaction({ value: '0x0', data: '0x', gasLimit: '0x5208', maxFeePerGas: '0x5208', maxPriorityFeePerGas: '0x5208', nonce: '0x0', }); const screen = response.getInterface(); expect(screen).toRender(panel([text('Hello, world!')])); }); }); ``` ### `snap.onSignature` The `onSignature` function can be used to send a signature request to the snap. It takes a single argument, which is an object with the following properties: - `origin`: The origin of the signature request. - `from`: The address of the signer. - `data`: The data of the signature. - `signatureMethod`: The signature method being used in the request. 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 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. ```js import { installSnap } from '@metamask/snaps-jest'; import { panel, text } from '@metamask/snaps-sdk'; describe('MySnap', () => { it('should do something', async () => { const { onSignature } = await installSnap(/* optional snap ID */); const response = await onSignature(); const screen = response.getInterface(); expect(screen).toRender( panel([text('You are using the personal_sign method')]), ); }); }); ``` ### `snap.onCronjob` The `onCronjob` function can be used to run a cronjob in the snap. It takes a single argument, which is similar to a JSON-RPC request object. It returns a promise that resolves to the response from the [onCronjob](https://docs.metamask.io/snaps/reference/exports/#oncronjob) function. The request would normally be specified in the snap manifest under the `endowment:cronjob` permission, but this function allows you to run cronjobs that are not specified in the manifest as well. ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { onCronjob } = await installSnap(/* optional snap ID */); const response = await onCronjob({ method: 'foo', params: [], }); // ... }); }); ``` It returns an object with a response, and some additional metadata, which can be checked using the [Jest matchers](#jest-matchers): ```js import { installSnap } from '@metamask/snaps-jest'; describe('MySnap', () => { it('should do something', async () => { const { onCronjob } = await installSnap(/* optional snap ID */); const response = await onCronjob({ method: 'foo', params: [], }); expect(response).toRespondWith('bar'); expect(response).not.toRespondWithError('baz'); }); }); ``` ### `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 contains a `getInterface` function to get the response from the [onHomePage](https://docs.metamask.io/snaps/reference/entry-points/#onhomepage) function. ```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(/* ... */); }); }); ``` ### `snap.onKeyringRequest` The `onKeyringRequest` function can be used to process keyring request. It takes few arguments, which are similar to a JSON-RPC request object. It returns a promise that resolves to the response from the keyring request handler. ```js import { installSnap } from '@metamask/snaps-jest'; describe('onKeyringRequest', () => { it('sends keyring request', async () => { const { onKeyringRequest } = await installSnap(); const response = await onKeyringRequest({ origin: 'https://metamask.github.io', params: { options: { privateKey: 'foo-bar', }, }, method: 'keyring_createAccount', }); expect(response).toBe({ /* Add expected result here */ }); }); }); ``` It returns an object with a response, and some additional metadata, which can be checked using the [Jest matchers](#jest-matchers): ### Jest matchers `@metamask/snaps-jest` includes a set of Jest matchers that can be used to assert that a response from a snap matches an expected value: - `toRespondWith(expectedResponse)`: Check if a response matches an expected response. This matcher will check the `result` property of the response. If the response is an error, it will fail. - `toRespondWithError(expectedError)`: Check if a response matches an expected error. This matcher will check the `error` property of the response. If the response is not an error, it will fail. - `toSendNotification(notificationText)`: Check if a snap sent a notification. - `toRender(expectedInterface)`: Check if a snap rendered an interface. This is useful for testing the UI of a snap, either for a [`snap_dialog`](https://docs.metamask.io/snaps/reference/rpc-api/#snap_dialog), UI rendered by [the transaction insights API](https://docs.metamask.io/snaps/reference/exports/#ontransaction) or [the signature insights API](https://docs.metamask.io/snaps/reference/exports/#onsignature). ### 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 ```js import { installSnap, assertIsAlertDialog } from '@metamask/snaps-jest'; import { text } from '@metamask/snaps-sdk'; import { assert } from '@metamask/utils'; describe('MySnap', () => { it('should do something', async () => { const { request } = await installSnap(/* optional snap ID */); // Note: We cannot resolve the promise yet! const response = request({ method: 'foo', }); const ui = await response.getInterface(); // This is useful if you're using TypeScript, since it will infer the type // of the user interface. assertIsAlertDialog(ui); expect(ui).toRender(text('Hello, world!')); // "Click" the OK button. await ui.ok(); // Now we can resolve the promise. const result = await response; expect(result).toRespondWith('bar'); }); }); ``` #### 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 `testEnvironmentOptions` property to your Jest configuration. For example: ```js module.exports = { preset: '@metamask/snaps-jest', testEnvironmentOptions: { // Options go here. }, }; ``` All options are optional, and have sensible defaults. ### `server` - Type: `object` Options for the built-in HTTP server that is included with this package. This server is used to serve the execution environment, simulator, and the snap bundle during tests. #### `server.enabled` - Type: `boolean` - Default: `true` Whether to enable the built-in HTTP server. By default, it will be enabled. If you want to use your own HTTP server, you can disable this option, and use the `executionEnvironmentUrl` and `simulatorUrl` options to configure the URLs of your own server. ##### Example ```js module.exports = { preset: '@metamask/snaps-jest', testEnvironmentOptions: { server: { enabled: false, }, }, }; ``` #### `server.port` - Type: `number` The port to use for the built-in HTTP server. By default, it will use a random available (unprivileged) port. ##### Example ```js module.exports = { preset: '@metamask/snaps-jest', testEnvironmentOptions: { server: { port: 8080, }, }, }; ``` #### `server.root` - Type: `string` - Default: `process.cwd()` The root directory to serve the snap files from. By default, it will use the current working directory. This is useful if you want to serve the snap files from a different directory than the one that Jest is running from. ##### Example ```js module.exports = { preset: '@metamask/snaps-jest', testEnvironmentOptions: { server: { root: '/path/to/snap/files', }, }, }; ```