
Research
Security News
Lazarus Strikes npm Again with New Wave of Malicious Packages
The Socket Research Team has discovered six new malicious npm packages linked to North Korea’s Lazarus Group, designed to steal credentials and deploy backdoors.
@metamask/snaps-jest
Advanced tools
A Jest preset for end-to-end testing MetaMask Snaps, including a Jest environment, and a set of Jest matchers.
@metamask/snaps-jest
A Jest preset for end-to-end testing MetaMask Snaps, including a Jest environment, and a set of Jest matchers. This package is currently experimental, and the API may change in the future.
Use Node.js 16.0.0
or later. We recommend using nvm for managing Node.js versions.
Install a dependency in your snap project using yarn
(or npm
):
yarn add -D @metamask/snaps-jest
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:
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 theroot
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.
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:
module.exports = {
testEnvironment: '@metamask/snaps-jest',
setupFilesAfterEnv: ['@metamask/snaps-jest/dist/cjs/setup.js'],
};
@metamask/snaps-jest
exposes a installSnap
function that can be used to
install a snap in the browser. 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.
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
function.
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:
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');
});
});
If you prefer, you can also manually check the response:
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:
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.sendTransaction
The sendTransaction
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 an object with the user interface that was shown by the snap, in the onTransaction function.
import { installSnap } from '@metamask/snaps-jest';
import { panel, text } from '@metamask/snaps-ui';
describe('MySnap', () => {
it('should do something', async () => {
const { sendTransaction } = await installSnap(/* optional snap ID */);
const response = await sendTransaction({
value: '0x0',
data: '0x',
gasLimit: '0x5208',
maxFeePerGas: '0x5208',
maxPriorityFeePerGas: '0x5208',
nonce: '0x0',
});
expect(response).toRender(panel([text('Hello, world!')]));
});
});
snap.runCronjob
The runCronjob
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
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.
import { installSnap } from '@metamask/snaps-jest';
describe('MySnap', () => {
it('should do something', async () => {
const { runCronjob } = await installSnap(/* optional snap ID */);
const response = await runCronjob({
method: 'foo',
params: [],
});
// ...
});
});
It returns an object with a response, and some additional metadata, which can be checked using the Jest matchers:
import { installSnap } from '@metamask/snaps-jest';
describe('MySnap', () => {
it('should do something', async () => {
const { runCronjob } = await installSnap(/* optional snap ID */);
const response = await runCronjob({
method: 'foo',
params: [],
});
expect(response).toRespondWith('bar');
expect(response).not.toRespondWithError('baz');
});
});
snap.close
The close
function can be used to close the page that the test is running in.
This is mainly useful for cleaning up after a test, and is not required. It can
potentially speed up your tests, since it prevents too many pages from being
open at the same time.
import { installSnap } from '@metamask/snaps-jest';
describe('MySnap', () => {
it('should do something', async () => {
const { close } = await installSnap(/* optional snap ID */);
// ...
await close();
});
});
@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
,
or a UI rendered by
the transaction insights API.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.
import { installSnap } from '@metamask/snaps-jest';
import { text } from '@metamask/snaps-ui';
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.
assert(ui.type === 'alert');
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');
});
});
snap.mock
)@metamask/snaps-jest
includes basic network mocking functionality. This can
be used to mock the response of any network request made by the snap through
the endowment:network-access
functionality.
The mock function takes a single argument, which is an object with the following properties:
url
(string | RegExp
): The URL of the request. This can be a string, or a
regular expression.partial
(boolean
): If enabled, the request will be mocked if the URL
starts with the given URL. This option is ignored if a RegExp is provided to
the url
option. Defaults to false
.response
(object
): An object with the response.
status
(number
): The status code of the response. Defaults to 200
.body
(string
): The body of the response. Defaults to an empty string.headers
(object
): An object with the headers of the response. By
default, this will use headers that enable CORS.contentType
(string
): The content type of the response. Defaults to
text/plain
.Except for the url
option, all options are optional.
mock.unmock
)The mock function returns an object with an unmock
function that can be
used to remove the mock. Mocking happens on a per-snap-install basis. If the
mock is not removed, it will remain active for the rest of the snap
installation, so it does not affect other tests with a fresh snap installation.
import { installSnap } from '@metamask/snaps-jest';
describe('MySnap', () => {
it('should do something', async () => {
const { mock, request } = await installSnap(/* optional snap ID */);
const { unmock } = mock({
url: 'https://example.com',
status: 200,
body: 'Hello, world!',
});
// ...
unmock();
});
});
You can pass options to the test environment by adding a
testEnvironmentOptions
property to your Jest configuration. For example:
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
// Options go here.
},
};
All options are optional, and have sensible defaults.
keepAlive
boolean
false
Whether to keep the Jest environment running after the tests have finished. This
is useful for debugging, but should not be used in CI environments: When this is
enabled, the jest
process will not exit on its own, and will need to be
manually killed (e.g., with Ctrl + C).
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
keepAlive: true,
},
};
browser
object
Options for the browser that is used to run the tests.
browser.headless
boolean
true
Whether to run the browser in headless mode. By default, it will be enabled. If
you want to see the browser window while the tests are running, you can disable
this option. Note that this will require you to have a graphical environment
available, so it is not recommended for CI environments, but can be useful for
debugging in conjunction with the keepAlive
option.
server
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
boolean
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.
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
server: {
enabled: false,
},
},
};
server.port
number
The port to use for the built-in HTTP server. By default, it will use a random available (unprivileged) port.
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
server: {
port: 8080,
},
},
};
server.root
string
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.
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
server: {
root: '/path/to/snap/files',
},
},
};
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
browser: {
headless: false,
},
},
};
executionEnvironmentUrl
string
The URL of the execution environment to use for testing. This is the URL that will be loaded by the Snaps Simulator in the tests. By default, it will use the URL of the built-in HTTP server that is included with this package.
Note: This option is intended for advanced use cases. In most cases, you should not need to change this option.
See also: @metamask/snaps-execution-environments
.
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
executionEnvironmentUrl: 'http://localhost:8080',
},
};
simulatorUrl
string
The URL of the simulator to use for testing. This is the URL that will be loaded in the browser when running tests. By default, it will use the URL of the built-in HTTP server that is included with this package.
Note: This option is intended for advanced use cases. In most cases, you should not need to change this option.
See also: @metamask/snaps-simulator
.
module.exports = {
preset: '@metamask/snaps-jest',
testEnvironmentOptions: {
simulatorUrl: 'http://localhost:8081',
},
};
FAQs
A Jest preset for end-to-end testing MetaMask Snaps, including a Jest environment, and a set of Jest matchers
The npm package @metamask/snaps-jest receives a total of 529 weekly downloads. As such, @metamask/snaps-jest popularity was classified as not popular.
We found that @metamask/snaps-jest demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
The Socket Research Team has discovered six new malicious npm packages linked to North Korea’s Lazarus Group, designed to steal credentials and deploy backdoors.
Security News
Socket CEO Feross Aboukhadijeh discusses the open web, open source security, and how Socket tackles software supply chain attacks on The Pair Program podcast.
Security News
Opengrep continues building momentum with the alpha release of its Playground tool, demonstrating the project's rapid evolution just two months after its initial launch.