diff --git a/packages/snaps-jest/package.json b/packages/snaps-jest/package.json index e4fa82b74e..e6126eb4f7 100644 --- a/packages/snaps-jest/package.json +++ b/packages/snaps-jest/package.json @@ -42,28 +42,15 @@ "@jest/environment": "^29.5.0", "@jest/expect": "^29.5.0", "@jest/globals": "^29.5.0", - "@metamask/base-controller": "^6.0.2", - "@metamask/eth-json-rpc-middleware": "^14.0.0", - "@metamask/json-rpc-engine": "^9.0.2", - "@metamask/json-rpc-middleware-stream": "^8.0.2", - "@metamask/key-tree": "^9.1.2", - "@metamask/permission-controller": "^11.0.0", - "@metamask/phishing-controller": "^12.0.2", "@metamask/snaps-controllers": "workspace:^", - "@metamask/snaps-execution-environments": "workspace:^", - "@metamask/snaps-rpc-methods": "workspace:^", "@metamask/snaps-sdk": "workspace:^", - "@metamask/snaps-utils": "workspace:^", + "@metamask/snaps-simulation": "workspace:^", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^9.2.1", - "@reduxjs/toolkit": "^1.9.5", "express": "^4.18.2", "jest-environment-node": "^29.5.0", "jest-matcher-utils": "^29.5.0", - "mime": "^3.0.0", - "readable-stream": "^3.6.2", - "redux": "^4.2.1", - "redux-saga": "^1.2.3" + "redux": "^4.2.1" }, "devDependencies": { "@jest/types": "^29.6.3", @@ -73,11 +60,11 @@ "@metamask/eslint-config-jest": "^12.1.0", "@metamask/eslint-config-nodejs": "^12.1.0", "@metamask/eslint-config-typescript": "^12.1.0", + "@metamask/snaps-utils": "workspace:^", "@swc/core": "1.3.78", "@swc/jest": "^0.2.26", "@ts-bridge/cli": "^0.5.1", "@types/jest": "^27.5.1", - "@types/mime": "^3.0.0", "@types/semver": "^7.5.0", "@typescript-eslint/eslint-plugin": "^5.42.1", "@typescript-eslint/parser": "^6.21.0", diff --git a/packages/snaps-jest/src/environment.ts b/packages/snaps-jest/src/environment.ts index 9ca1d20202..cd3f01f606 100644 --- a/packages/snaps-jest/src/environment.ts +++ b/packages/snaps-jest/src/environment.ts @@ -4,13 +4,17 @@ import type { } from '@jest/environment'; import type { AbstractExecutionService } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; +import type { + InstalledSnap, + InstallSnapOptions, +} from '@metamask/snaps-simulation'; +import { installSnap } from '@metamask/snaps-simulation'; import { assert, createModuleLogger } from '@metamask/utils'; import type { Server } from 'http'; import NodeEnvironment from 'jest-environment-node'; import type { AddressInfo } from 'net'; -import type { InstalledSnap, InstallSnapOptions } from './internals'; -import { handleInstallSnap, rootLogger, startServer } from './internals'; +import { rootLogger, startServer } from './internals'; import type { SnapsEnvironmentOptions } from './options'; import { getOptions } from './options'; @@ -88,7 +92,7 @@ export class SnapsEnvironment extends NodeEnvironment { options: Partial> = {}, ) { await this.#instance?.executionService.terminateAllSnaps(); - this.#instance = await handleInstallSnap(snapId as SnapId, options); + this.#instance = await installSnap(snapId as SnapId, options); return this.#instance; } diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx index 1867def9d9..87b9400715 100644 --- a/packages/snaps-jest/src/helpers.test.tsx +++ b/packages/snaps-jest/src/helpers.test.tsx @@ -1,16 +1,16 @@ import { NodeProcessExecutionService } from '@metamask/snaps-controllers/node'; import { DialogType } from '@metamask/snaps-sdk'; import { Text } from '@metamask/snaps-sdk/jsx'; -import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; - +import type { InstallSnapOptions } from '@metamask/snaps-simulation'; import { assertIsAlertDialog, assertIsConfirmationDialog, assertIsPromptDialog, - installSnap, -} from './helpers'; -import type { InstallSnapOptions } from './internals'; -import { handleInstallSnap } from './internals'; + installSnap as simulateSnap, +} from '@metamask/snaps-simulation'; +import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; + +import { installSnap } from './helpers'; import { getMockServer } from './test-utils'; describe('installSnap', () => { @@ -18,7 +18,7 @@ describe('installSnap', () => { Object.defineProperty(global, 'snapsEnvironment', { writable: true, value: { - installSnap: handleInstallSnap, + installSnap: simulateSnap, }, }); }); @@ -215,7 +215,7 @@ describe('installSnap', () => { writable: true, value: { installSnap: async (_: string, options: InstallSnapOptions) => { - return handleInstallSnap(snapId, options); + return simulateSnap(snapId, options); }, }, }); @@ -260,7 +260,7 @@ describe('installSnap', () => { writable: true, value: { installSnap: async (_: string, options: InstallSnapOptions) => { - return handleInstallSnap(snapId, options); + return simulateSnap(snapId, options); }, }, }); diff --git a/packages/snaps-jest/src/helpers.ts b/packages/snaps-jest/src/helpers.ts index 4c6c8f454a..dbfbeb247b 100644 --- a/packages/snaps-jest/src/helpers.ts +++ b/packages/snaps-jest/src/helpers.ts @@ -1,31 +1,20 @@ import type { AbstractExecutionService } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; -import { DialogType } from '@metamask/snaps-sdk'; -import type { FooterElement } from '@metamask/snaps-sdk/jsx'; -import { HandlerType, getJsxChildren, logInfo } from '@metamask/snaps-utils'; -import { create } from '@metamask/superstruct'; -import { - assert, - assertStruct, - createModuleLogger, - hasProperty, -} from '@metamask/utils'; - +import type { InstallSnapOptions } from '@metamask/snaps-simulation'; import { - rootLogger, - handleRequest, - TransactionOptionsStruct, - getEnvironment, JsonRpcMockOptionsStruct, SignatureOptionsStruct, - SnapResponseWithInterfaceStruct, - getElementByType, -} from './internals'; -import type { InstallSnapOptions } from './internals'; -import { + handleRequest, + TransactionOptionsStruct, addJsonRpcMock, removeJsonRpcMock, -} from './internals/simulation/store/mocks'; + SnapResponseWithInterfaceStruct, +} from '@metamask/snaps-simulation'; +import { HandlerType, logInfo } from '@metamask/snaps-utils'; +import { create } from '@metamask/superstruct'; +import { assertStruct, createModuleLogger } from '@metamask/utils'; + +import { rootLogger, getEnvironment } from './internals'; import type { SnapResponseWithInterface, CronjobOptions, @@ -33,15 +22,6 @@ import type { Snap, SnapResponse, TransactionOptions, - SnapInterface, - SnapAlertInterface, - SnapInterfaceActions, - SnapConfirmationInterface, - SnapPromptInterface, - DefaultSnapInterface, - DefaultSnapInterfaceWithFooter, - DefaultSnapInterfaceWithPartialFooter, - DefaultSnapInterfaceWithoutFooter, } from './types'; const log = createModuleLogger(rootLogger, 'helpers'); @@ -79,89 +59,6 @@ function assertIsResponseWithInterface( assertStruct(response, SnapResponseWithInterfaceStruct); } -/** - * Ensure that the actual interface is an alert dialog. - * - * @param ui - The interface to verify. - */ -export function assertIsAlertDialog( - ui: SnapInterface, -): asserts ui is SnapAlertInterface & SnapInterfaceActions { - assert(hasProperty(ui, 'type') && ui.type === DialogType.Alert); -} - -/** - * Ensure that the actual interface is a confirmation dialog. - * - * @param ui - The interface to verify. - */ -export function assertIsConfirmationDialog( - ui: SnapInterface, -): asserts ui is SnapConfirmationInterface & SnapInterfaceActions { - assert(hasProperty(ui, 'type') && ui.type === DialogType.Confirmation); -} - -/** - * Ensure that the actual interface is a Prompt dialog. - * - * @param ui - The interface to verify. - */ -export function assertIsPromptDialog( - ui: SnapInterface, -): asserts ui is SnapPromptInterface & SnapInterfaceActions { - assert(hasProperty(ui, 'type') && ui.type === DialogType.Prompt); -} - -/** - * Ensure that the actual interface is a custom dialog. - * - * @param ui - The interface to verify. - */ -export function assertIsCustomDialog( - ui: SnapInterface, -): asserts ui is DefaultSnapInterface & SnapInterfaceActions { - assert(!hasProperty(ui, 'type')); -} - -/** - * Ensure that the actual interface is a custom dialog with a complete footer. - * - * @param ui - The interface to verify. - */ -export function assertCustomDialogHasFooter( - ui: DefaultSnapInterface & SnapInterfaceActions, -): asserts ui is DefaultSnapInterfaceWithFooter & SnapInterfaceActions { - const footer = getElementByType(ui.content, 'Footer'); - - assert(footer && getJsxChildren(footer).length === 2); -} - -/** - * Ensure that the actual interface is a custom dialog with a partial footer. - * - * @param ui - The interface to verify. - */ -export function assertCustomDialogHasPartialFooter( - ui: DefaultSnapInterface & SnapInterfaceActions, -): asserts ui is DefaultSnapInterfaceWithPartialFooter & SnapInterfaceActions { - const footer = getElementByType(ui.content, 'Footer'); - - assert(footer && getJsxChildren(footer).length === 1); -} - -/** - * Ensure that the actual interface is a custom dialog without a footer. - * - * @param ui - The interface to verify. - */ -export function assertCustomDialogHasNoFooter( - ui: DefaultSnapInterface & SnapInterfaceActions, -): asserts ui is DefaultSnapInterfaceWithoutFooter & SnapInterfaceActions { - const footer = getElementByType(ui.content, 'Footer'); - - assert(!footer); -} - /** * Load a snap into the environment. This is the main entry point for testing * snaps: It returns a {@link Snap} object that can be used to interact with the diff --git a/packages/snaps-jest/src/index.ts b/packages/snaps-jest/src/index.ts index 030365797a..f97525de0f 100644 --- a/packages/snaps-jest/src/index.ts +++ b/packages/snaps-jest/src/index.ts @@ -5,3 +5,12 @@ export { default, default as TestEnvironment } from './environment'; export * from './helpers'; export * from './options'; export * from './types'; + +export { + assertCustomDialogHasNoFooter, + assertCustomDialogHasPartialFooter, + assertIsAlertDialog, + assertIsConfirmationDialog, + assertIsCustomDialog, + assertIsPromptDialog, +} from '@metamask/snaps-simulation'; diff --git a/packages/snaps-jest/src/internals/index.ts b/packages/snaps-jest/src/internals/index.ts index fdbb16efe3..854d4d5a81 100644 --- a/packages/snaps-jest/src/internals/index.ts +++ b/packages/snaps-jest/src/internals/index.ts @@ -1,6 +1,3 @@ export * from './environment'; export * from './logger'; -export * from './request'; export * from './server'; -export * from './simulation'; -export * from './structs'; diff --git a/packages/snaps-jest/src/matchers.ts b/packages/snaps-jest/src/matchers.ts index f4a38e3156..471d5cf25b 100644 --- a/packages/snaps-jest/src/matchers.ts +++ b/packages/snaps-jest/src/matchers.ts @@ -13,6 +13,10 @@ import type { } from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; import { isJSXElementUnsafe } from '@metamask/snaps-sdk/jsx'; +import { + InterfaceStruct, + SnapResponseStruct, +} from '@metamask/snaps-simulation'; import { getJsxElementFromComponent, serialiseJsx, @@ -31,7 +35,6 @@ import { RECEIVED_COLOR, } from 'jest-matcher-utils'; -import { InterfaceStruct, SnapResponseStruct } from './internals'; import type { SnapResponse } from './types'; /** diff --git a/packages/snaps-jest/src/test-utils/index.ts b/packages/snaps-jest/src/test-utils/index.ts index 276c74b205..581cbaf872 100644 --- a/packages/snaps-jest/src/test-utils/index.ts +++ b/packages/snaps-jest/src/test-utils/index.ts @@ -1,5 +1,4 @@ export * from './jest'; -export * from './options'; export * from './response'; export * from './server'; export * from './controller'; diff --git a/packages/snaps-jest/src/test-utils/response.ts b/packages/snaps-jest/src/test-utils/response.ts index 7e3c20c0f0..1babfeed76 100644 --- a/packages/snaps-jest/src/test-utils/response.ts +++ b/packages/snaps-jest/src/test-utils/response.ts @@ -38,5 +38,8 @@ export function getMockInterfaceResponse( content, clickElement: jest.fn(), typeInField: jest.fn(), + selectInDropdown: jest.fn(), + selectFromRadioGroup: jest.fn(), + uploadFile: jest.fn(), }; } diff --git a/packages/snaps-jest/src/types/types.ts b/packages/snaps-jest/src/types/types.ts index 0d3eedd3ae..a96b4ae1e4 100644 --- a/packages/snaps-jest/src/types/types.ts +++ b/packages/snaps-jest/src/types/types.ts @@ -1,15 +1,14 @@ import type { NotificationType, EnumToUnion } from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; -import type { InferMatching } from '@metamask/snaps-utils'; -import type { Infer } from '@metamask/superstruct'; -import type { Json, JsonRpcId, JsonRpcParams } from '@metamask/utils'; - import type { SignatureOptionsStruct, SnapOptionsStruct, SnapResponseStruct, TransactionOptionsStruct, -} from '../internals'; +} from '@metamask/snaps-simulation'; +import type { InferMatching } from '@metamask/snaps-utils'; +import type { Infer } from '@metamask/superstruct'; +import type { Json, JsonRpcId, JsonRpcParams } from '@metamask/utils'; export type RequestOptions = { /** diff --git a/packages/snaps-jest/tsconfig.build.json b/packages/snaps-jest/tsconfig.build.json index e19a4c36e4..a8924e15d4 100644 --- a/packages/snaps-jest/tsconfig.build.json +++ b/packages/snaps-jest/tsconfig.build.json @@ -14,20 +14,11 @@ "./src/**/__snapshots__" ], "references": [ - { - "path": "../snaps-controllers/tsconfig.build.json" - }, - { - "path": "../snaps-execution-environments/tsconfig.build.json" - }, - { - "path": "../snaps-rpc-methods/tsconfig.build.json" - }, { "path": "../snaps-sdk/tsconfig.build.json" }, { - "path": "../snaps-utils/tsconfig.build.json" + "path": "../snaps-simulation/tsconfig.build.json" } ] } diff --git a/packages/snaps-jest/tsconfig.json b/packages/snaps-jest/tsconfig.json index 5816f86b5b..0768badb99 100644 --- a/packages/snaps-jest/tsconfig.json +++ b/packages/snaps-jest/tsconfig.json @@ -5,20 +5,11 @@ }, "include": ["./src", "package.json"], "references": [ - { - "path": "../snaps-controllers" - }, - { - "path": "../snaps-execution-environments" - }, - { - "path": "../snaps-rpc-methods" - }, { "path": "../snaps-sdk" }, { - "path": "../snaps-utils" + "path": "../snaps-simulation" } ] } diff --git a/packages/snaps-simulation/.depcheckrc.json b/packages/snaps-simulation/.depcheckrc.json new file mode 100644 index 0000000000..c36ffc77ce --- /dev/null +++ b/packages/snaps-simulation/.depcheckrc.json @@ -0,0 +1,18 @@ +{ + "ignore-patterns": ["dist", "coverage"], + "ignores": [ + "@lavamoat/allow-scripts", + "@lavamoat/preinstall-always-fail", + "@metamask/auto-changelog", + "@metamask/eslint-*", + "@types/*", + "@typescript-eslint/*", + "eslint-config-*", + "eslint-plugin-*", + "prettier-plugin-packagejson", + "ts-jest", + "ts-node", + "typedoc", + "typescript" + ] +} diff --git a/packages/snaps-simulation/.eslintrc.js b/packages/snaps-simulation/.eslintrc.js new file mode 100644 index 0000000000..c3b7e0a34a --- /dev/null +++ b/packages/snaps-simulation/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + + parserOptions: { + tsconfigRootDir: __dirname, + }, + + overrides: [ + { + files: ['*.test.ts'], + rules: { + 'jest/expect-expect': [ + 'error', + { + assertFunctionNames: ['expect', 'expectTypeOf'], + }, + ], + }, + }, + ], +}; diff --git a/packages/snaps-simulation/CHANGELOG.md b/packages/snaps-simulation/CHANGELOG.md new file mode 100644 index 0000000000..aa399df1be --- /dev/null +++ b/packages/snaps-simulation/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/snaps/ diff --git a/packages/snaps-simulation/LICENSE b/packages/snaps-simulation/LICENSE new file mode 100644 index 0000000000..4a4c080fa7 --- /dev/null +++ b/packages/snaps-simulation/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2024 MetaMask + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/snaps-simulation/README.md b/packages/snaps-simulation/README.md new file mode 100644 index 0000000000..fd962ca4ce --- /dev/null +++ b/packages/snaps-simulation/README.md @@ -0,0 +1,14 @@ +# @metamask/snaps-simulation + +A simulation framework for MetaMask Snaps, which runs in Node.js, and can be +used to test Snaps in a headless environment. + +## Installation + +Use Node.js `18.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`: + +- `npm install @metamask/snaps-simulation` +- `yarn add @metamask/snaps-simulation` diff --git a/packages/snaps-simulation/jest.config.js b/packages/snaps-simulation/jest.config.js new file mode 100644 index 0000000000..b187b6e932 --- /dev/null +++ b/packages/snaps-simulation/jest.config.js @@ -0,0 +1,25 @@ +const deepmerge = require('deepmerge'); + +const baseConfig = require('../../jest.config.base'); + +module.exports = deepmerge(baseConfig, { + testTimeout: 30000, + + collectCoverageFrom: [ + '!./src/**/index.ts', + '!./src/types/global.ts', + '!./src/types/images.ts', + ], + + transform: { + '^.+\\.(t|j)sx?$': [ + 'ts-jest', + { + tsconfig: { + jsx: 'react-jsx', + jsxImportSource: '@metamask/snaps-sdk', + }, + }, + ], + }, +}); diff --git a/packages/snaps-simulation/package.json b/packages/snaps-simulation/package.json new file mode 100644 index 0000000000..70a4ed7a21 --- /dev/null +++ b/packages/snaps-simulation/package.json @@ -0,0 +1,101 @@ +{ + "name": "@metamask/snaps-simulation", + "version": "0.0.0", + "description": "A simulation framework for MetaMask Snaps, enabling headless testing of Snaps in a controlled environment", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/snaps.git" + }, + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "default": "./dist/index.cjs", + "types": "./dist/index.d.cts" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.cts", + "files": [ + "dist" + ], + "scripts": { + "test": "jest && yarn posttest", + "posttest": "jest-it-up --margin 0.25", + "test:ci": "yarn test", + "lint:eslint": "eslint . --cache --ext js,ts,jsx,tsx", + "lint:misc": "prettier --no-error-on-unmatched-pattern --loglevel warn \"**/*.json\" \"**/*.md\" \"**/*.html\" \"!CHANGELOG.md\" --ignore-path ../../.gitignore", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn lint:changelog && yarn lint:dependencies", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", + "lint:changelog": "../../scripts/validate-changelog.sh @metamask/snaps-simulation", + "build": "ts-bridge --project tsconfig.build.json --verbose --no-references", + "publish:preview": "yarn npm publish --tag preview", + "lint:ci": "yarn lint", + "lint:dependencies": "depcheck" + }, + "dependencies": { + "@metamask/base-controller": "^6.0.2", + "@metamask/eth-json-rpc-middleware": "^14.0.0", + "@metamask/json-rpc-engine": "^9.0.2", + "@metamask/json-rpc-middleware-stream": "^8.0.2", + "@metamask/key-tree": "^9.1.2", + "@metamask/permission-controller": "^11.0.0", + "@metamask/phishing-controller": "^12.0.2", + "@metamask/snaps-controllers": "workspace:^", + "@metamask/snaps-rpc-methods": "workspace:^", + "@metamask/snaps-sdk": "workspace:^", + "@metamask/snaps-utils": "workspace:^", + "@metamask/superstruct": "^3.1.0", + "@metamask/utils": "^9.2.1", + "@reduxjs/toolkit": "^1.9.5", + "mime": "^3.0.0", + "readable-stream": "^3.6.2", + "redux-saga": "^1.2.3" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^3.0.4", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eslint-config": "^12.1.0", + "@metamask/eslint-config-jest": "^12.1.0", + "@metamask/eslint-config-nodejs": "^12.1.0", + "@metamask/eslint-config-typescript": "^12.1.0", + "@ts-bridge/cli": "^0.5.1", + "@types/express": "^4.17.17", + "@types/jest": "^27.5.1", + "@types/mime": "^3.0.0", + "@types/readable-stream": "^4.0.15", + "@typescript-eslint/eslint-plugin": "^5.42.1", + "@typescript-eslint/parser": "^6.21.0", + "deepmerge": "^4.2.2", + "depcheck": "^1.4.7", + "eslint": "^8.27.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^27.1.5", + "eslint-plugin-jsdoc": "^41.1.2", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "express": "^4.18.2", + "jest": "^29.0.2", + "jest-it-up": "^2.0.0", + "prettier": "^2.7.1", + "prettier-plugin-packagejson": "^2.2.11", + "ts-jest": "^29.1.1", + "typescript": "~5.3.3" + }, + "engines": { + "node": "^18.16 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/snaps-jest/src/internals/simulation/constants.ts b/packages/snaps-simulation/src/constants.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/constants.ts rename to packages/snaps-simulation/src/constants.ts diff --git a/packages/snaps-jest/src/internals/simulation/controllers.test.ts b/packages/snaps-simulation/src/controllers.test.ts similarity index 94% rename from packages/snaps-jest/src/internals/simulation/controllers.test.ts rename to packages/snaps-simulation/src/controllers.test.ts index 968e5d8a9c..f95d661a01 100644 --- a/packages/snaps-jest/src/internals/simulation/controllers.test.ts +++ b/packages/snaps-simulation/src/controllers.test.ts @@ -4,9 +4,9 @@ import { SubjectMetadataController, } from '@metamask/permission-controller'; -import { getMockOptions } from '../../test-utils/options'; import { getControllers } from './controllers'; import type { MiddlewareHooks } from './simulation'; +import { getMockOptions } from './test-utils'; const MOCK_HOOKS: MiddlewareHooks = { getIsLocked: jest.fn(), diff --git a/packages/snaps-jest/src/internals/simulation/controllers.ts b/packages/snaps-simulation/src/controllers.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/controllers.ts rename to packages/snaps-simulation/src/controllers.ts diff --git a/packages/snaps-jest/src/internals/simulation/files.test.ts b/packages/snaps-simulation/src/files.test.ts similarity index 95% rename from packages/snaps-jest/src/internals/simulation/files.test.ts rename to packages/snaps-simulation/src/files.test.ts index 9f485e7fc6..1b7fefc852 100644 --- a/packages/snaps-jest/src/internals/simulation/files.test.ts +++ b/packages/snaps-simulation/src/files.test.ts @@ -56,13 +56,13 @@ describe('getFileSize', () => { it('returns the file size for a file path', async () => { expect( - await getFileSize(resolve(__dirname, '../../test-utils/snap/snap.js')), + await getFileSize(resolve(__dirname, './test-utils/snap/snap.js')), ).toBe(112); }); }); describe('getFileToUpload', () => { - const MOCK_FILE = resolve(__dirname, '../../test-utils/snap/snap.js'); + const MOCK_FILE = resolve(__dirname, './test-utils/snap/snap.js'); it('returns the file object', async () => { const file = await getFileToUpload(MOCK_FILE, { diff --git a/packages/snaps-jest/src/internals/simulation/files.ts b/packages/snaps-simulation/src/files.ts similarity index 98% rename from packages/snaps-jest/src/internals/simulation/files.ts rename to packages/snaps-simulation/src/files.ts index 58d3d68b71..44229f4122 100644 --- a/packages/snaps-jest/src/internals/simulation/files.ts +++ b/packages/snaps-simulation/src/files.ts @@ -7,7 +7,7 @@ import { readFile, stat } from 'fs/promises'; import mime from 'mime'; import { basename, extname, resolve } from 'path'; -import type { FileOptions } from '../../types'; +import type { FileOptions } from './types'; /** * Get a statically defined Snap file from an array of files. diff --git a/packages/snaps-jest/src/internals/simulation/index.ts b/packages/snaps-simulation/src/index.ts similarity index 50% rename from packages/snaps-jest/src/internals/simulation/index.ts rename to packages/snaps-simulation/src/index.ts index 153f82b60a..7ff912556d 100644 --- a/packages/snaps-jest/src/internals/simulation/index.ts +++ b/packages/snaps-simulation/src/index.ts @@ -1,5 +1,10 @@ export * from './constants'; +export * from './controllers'; export * from './interface'; -export * from './simulation'; export * from './options'; +export * from './request'; +export * from './simulation'; export * from './store'; +export * from './structs'; +export * from './types'; +export * from './validation'; diff --git a/packages/snaps-jest/src/internals/simulation/interface.test.tsx b/packages/snaps-simulation/src/interface.test.tsx similarity index 99% rename from packages/snaps-jest/src/internals/simulation/interface.test.tsx rename to packages/snaps-simulation/src/interface.test.tsx index a511cab366..677537e4ca 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.test.tsx +++ b/packages/snaps-simulation/src/interface.test.tsx @@ -37,20 +37,6 @@ import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import type { SagaIterator } from 'redux-saga'; import { take } from 'redux-saga/effects'; -import { - assertIsAlertDialog, - assertIsConfirmationDialog, - assertIsCustomDialog, - assertIsPromptDialog, - assertCustomDialogHasFooter, - assertCustomDialogHasPartialFooter, - assertCustomDialogHasNoFooter, -} from '../../helpers'; -import { - getMockOptions, - getRestrictedSnapInterfaceControllerMessenger, - getRootControllerMessenger, -} from '../../test-utils'; import { clickElement, getElement, @@ -66,6 +52,20 @@ import { } from './interface'; import type { RunSagaFunction } from './store'; import { createStore, resolveInterface, setInterface } from './store'; +import { + getMockOptions, + getRestrictedSnapInterfaceControllerMessenger, + getRootControllerMessenger, +} from './test-utils'; +import { + assertIsAlertDialog, + assertIsConfirmationDialog, + assertIsCustomDialog, + assertIsPromptDialog, + assertCustomDialogHasFooter, + assertCustomDialogHasPartialFooter, + assertCustomDialogHasNoFooter, +} from './validation'; /** * Wait for the `resolveInterface` action to be dispatched and return the diff --git a/packages/snaps-jest/src/internals/simulation/interface.ts b/packages/snaps-simulation/src/interface.ts similarity index 99% rename from packages/snaps-jest/src/internals/simulation/interface.ts rename to packages/snaps-simulation/src/interface.ts index cc1023ee35..1053b4fbed 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.ts +++ b/packages/snaps-simulation/src/interface.ts @@ -22,15 +22,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { type SagaIterator } from 'redux-saga'; import { call, put, select, take } from 'redux-saga/effects'; -import type { - FileOptions, - SnapInterface, - SnapInterfaceActions, -} from '../../types'; import type { RootControllerMessenger } from './controllers'; import { getFileSize, getFileToUpload } from './files'; import type { Interface, RunSagaFunction } from './store'; import { getCurrentInterface, resolveInterface, setInterface } from './store'; +import type { FileOptions, SnapInterface, SnapInterfaceActions } from './types'; /** * The maximum file size that can be uploaded. diff --git a/packages/snaps-jest/src/internals/simulation/methods/constants.ts b/packages/snaps-simulation/src/methods/constants.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/constants.ts rename to packages/snaps-simulation/src/methods/constants.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/get-preferences.test.ts b/packages/snaps-simulation/src/methods/hooks/get-preferences.test.ts similarity index 93% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/get-preferences.test.ts rename to packages/snaps-simulation/src/methods/hooks/get-preferences.test.ts index bf34aa1282..69a63f7238 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/hooks/get-preferences.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/get-preferences.test.ts @@ -1,4 +1,4 @@ -import { getMockOptions } from '../../../../test-utils/options'; +import { getMockOptions } from '../../test-utils/options'; import { getGetPreferencesMethodImplementation } from './get-preferences'; describe('getGetPreferencesMethodImplementation', () => { diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/get-preferences.ts b/packages/snaps-simulation/src/methods/hooks/get-preferences.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/get-preferences.ts rename to packages/snaps-simulation/src/methods/hooks/get-preferences.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/index.ts b/packages/snaps-simulation/src/methods/hooks/index.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/index.ts rename to packages/snaps-simulation/src/methods/hooks/index.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/interface.test.ts b/packages/snaps-simulation/src/methods/hooks/interface.test.ts similarity index 98% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/interface.test.ts rename to packages/snaps-simulation/src/methods/hooks/interface.test.ts index a08ee12186..ffad7cdaee 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/hooks/interface.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/interface.test.ts @@ -6,7 +6,7 @@ import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import { getRestrictedSnapInterfaceControllerMessenger, getRootControllerMessenger, -} from '../../../../test-utils'; +} from '../../test-utils'; import { getCreateInterfaceImplementation, getGetInterfaceImplementation, diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/interface.ts b/packages/snaps-simulation/src/methods/hooks/interface.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/interface.ts rename to packages/snaps-simulation/src/methods/hooks/interface.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/notifications.test.ts b/packages/snaps-simulation/src/methods/hooks/notifications.test.ts similarity index 96% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/notifications.test.ts rename to packages/snaps-simulation/src/methods/hooks/notifications.test.ts index 4c6c739596..09c430c4a2 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/hooks/notifications.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/notifications.test.ts @@ -1,7 +1,7 @@ import { NotificationType } from '@metamask/snaps-sdk'; -import { getMockOptions } from '../../../../test-utils'; import { createStore } from '../../store'; +import { getMockOptions } from '../../test-utils'; import { getShowInAppNotificationImplementation, getShowNativeNotificationImplementation, diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/notifications.ts b/packages/snaps-simulation/src/methods/hooks/notifications.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/notifications.ts rename to packages/snaps-simulation/src/methods/hooks/notifications.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/request-user-approval.test.ts b/packages/snaps-simulation/src/methods/hooks/request-user-approval.test.ts similarity index 92% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/request-user-approval.test.ts rename to packages/snaps-simulation/src/methods/hooks/request-user-approval.test.ts index b04731551d..2623b08c3c 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/hooks/request-user-approval.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/request-user-approval.test.ts @@ -1,8 +1,8 @@ import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { DialogType } from '@metamask/snaps-sdk'; -import { getMockOptions } from '../../../../test-utils'; import { createStore, resolveInterface } from '../../store'; +import { getMockOptions } from '../../test-utils'; import { getRequestUserApprovalImplementation } from './request-user-approval'; describe('getShowUserApprovalImplementation', () => { diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/request-user-approval.ts b/packages/snaps-simulation/src/methods/hooks/request-user-approval.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/request-user-approval.ts rename to packages/snaps-simulation/src/methods/hooks/request-user-approval.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/state.test.ts b/packages/snaps-simulation/src/methods/hooks/state.test.ts similarity index 98% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/state.test.ts rename to packages/snaps-simulation/src/methods/hooks/state.test.ts index 7218b242e0..a469de577c 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/hooks/state.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/state.test.ts @@ -1,7 +1,7 @@ import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; -import { getMockOptions } from '../../../../test-utils'; import { createStore, getState, setState } from '../../store'; +import { getMockOptions } from '../../test-utils'; import { getClearSnapStateMethodImplementation, getGetSnapStateMethodImplementation, diff --git a/packages/snaps-jest/src/internals/simulation/methods/hooks/state.ts b/packages/snaps-simulation/src/methods/hooks/state.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/hooks/state.ts rename to packages/snaps-simulation/src/methods/hooks/state.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/index.ts b/packages/snaps-simulation/src/methods/index.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/index.ts rename to packages/snaps-simulation/src/methods/index.ts diff --git a/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts similarity index 98% rename from packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts rename to packages/snaps-simulation/src/methods/specifications.test.ts index 8517da8304..0e3f7ea897 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -4,9 +4,9 @@ import { MOCK_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; -import { getMockOptions } from '../../../test-utils/options'; import { getControllers, registerSnap } from '../controllers'; import type { MiddlewareHooks } from '../simulation'; +import { getMockOptions } from '../test-utils/options'; import { asyncResolve, getEndowments, @@ -20,6 +20,8 @@ const MOCK_HOOKS: MiddlewareHooks = { createInterface: jest.fn(), updateInterface: jest.fn(), getInterfaceState: jest.fn(), + getIsLocked: jest.fn(), + resolveInterface: jest.fn(), }; describe('resolve', () => { diff --git a/packages/snaps-jest/src/internals/simulation/methods/specifications.ts b/packages/snaps-simulation/src/methods/specifications.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/methods/specifications.ts rename to packages/snaps-simulation/src/methods/specifications.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/engine.test.ts b/packages/snaps-simulation/src/middleware/engine.test.ts similarity index 77% rename from packages/snaps-jest/src/internals/simulation/middleware/engine.test.ts rename to packages/snaps-simulation/src/middleware/engine.test.ts index e55ac0c7d4..c11ade60c9 100644 --- a/packages/snaps-jest/src/internals/simulation/middleware/engine.test.ts +++ b/packages/snaps-simulation/src/middleware/engine.test.ts @@ -1,5 +1,5 @@ -import { getMockOptions } from '../../../test-utils'; import { createStore } from '../store'; +import { getMockOptions } from '../test-utils'; import { createJsonRpcEngine } from './engine'; describe('createJsonRpcEngine', () => { @@ -10,6 +10,11 @@ describe('createJsonRpcEngine', () => { hooks: { getMnemonic: jest.fn(), getSnapFile: jest.fn().mockResolvedValue('foo'), + getIsLocked: jest.fn(), + getInterfaceState: jest.fn(), + createInterface: jest.fn(), + updateInterface: jest.fn(), + resolveInterface: jest.fn(), }, permissionMiddleware: jest.fn(), }); diff --git a/packages/snaps-jest/src/internals/simulation/middleware/engine.ts b/packages/snaps-simulation/src/middleware/engine.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/engine.ts rename to packages/snaps-simulation/src/middleware/engine.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/index.ts b/packages/snaps-simulation/src/middleware/index.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/index.ts rename to packages/snaps-simulation/src/middleware/index.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/account.test.ts b/packages/snaps-simulation/src/middleware/internal-methods/account.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/account.test.ts rename to packages/snaps-simulation/src/middleware/internal-methods/account.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/accounts.ts b/packages/snaps-simulation/src/middleware/internal-methods/accounts.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/accounts.ts rename to packages/snaps-simulation/src/middleware/internal-methods/accounts.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/index.ts b/packages/snaps-simulation/src/middleware/internal-methods/index.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/index.ts rename to packages/snaps-simulation/src/middleware/internal-methods/index.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.test.ts b/packages/snaps-simulation/src/middleware/internal-methods/middleware.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.test.ts rename to packages/snaps-simulation/src/middleware/internal-methods/middleware.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts b/packages/snaps-simulation/src/middleware/internal-methods/middleware.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts rename to packages/snaps-simulation/src/middleware/internal-methods/middleware.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/provider-state.test.ts b/packages/snaps-simulation/src/middleware/internal-methods/provider-state.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/provider-state.test.ts rename to packages/snaps-simulation/src/middleware/internal-methods/provider-state.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/provider-state.ts b/packages/snaps-simulation/src/middleware/internal-methods/provider-state.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/internal-methods/provider-state.ts rename to packages/snaps-simulation/src/middleware/internal-methods/provider-state.ts diff --git a/packages/snaps-jest/src/internals/simulation/middleware/mock.test.ts b/packages/snaps-simulation/src/middleware/mock.test.ts similarity index 95% rename from packages/snaps-jest/src/internals/simulation/middleware/mock.test.ts rename to packages/snaps-simulation/src/middleware/mock.test.ts index 576c6f4eba..f9e675f60b 100644 --- a/packages/snaps-jest/src/internals/simulation/middleware/mock.test.ts +++ b/packages/snaps-simulation/src/middleware/mock.test.ts @@ -1,8 +1,8 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import { getMockOptions } from '../../../test-utils'; import { createStore } from '../store'; import { addJsonRpcMock } from '../store/mocks'; +import { getMockOptions } from '../test-utils'; import { createMockMiddleware } from './mock'; describe('createMockMiddleware', () => { diff --git a/packages/snaps-jest/src/internals/simulation/middleware/mock.ts b/packages/snaps-simulation/src/middleware/mock.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/middleware/mock.ts rename to packages/snaps-simulation/src/middleware/mock.ts diff --git a/packages/snaps-jest/src/internals/simulation/options.test.ts b/packages/snaps-simulation/src/options.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/options.test.ts rename to packages/snaps-simulation/src/options.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/options.ts b/packages/snaps-simulation/src/options.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/options.ts rename to packages/snaps-simulation/src/options.ts diff --git a/packages/snaps-jest/src/internals/request.test.tsx b/packages/snaps-simulation/src/request.test.tsx similarity index 88% rename from packages/snaps-jest/src/internals/request.test.tsx rename to packages/snaps-simulation/src/request.test.tsx index 2c80da79ca..60e3025c4e 100644 --- a/packages/snaps-jest/src/internals/request.test.tsx +++ b/packages/snaps-simulation/src/request.test.tsx @@ -11,20 +11,23 @@ import { SelectorOption, } from '@metamask/snaps-sdk/jsx'; import { getJsxElementFromComponent, HandlerType } from '@metamask/snaps-utils'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; - import { - getMockServer, - getRestrictedSnapInterfaceControllerMessenger, - getRootControllerMessenger, -} from '../test-utils'; -import type { SnapResponseWithInterface } from '../types'; + getSnapManifest, + MOCK_SNAP_ID, +} from '@metamask/snaps-utils/test-utils'; + import { getInterfaceApi, getInterfaceFromResult, handleRequest, } from './request'; -import { handleInstallSnap } from './simulation'; +import { installSnap } from './simulation'; +import { + getMockServer, + getRestrictedSnapInterfaceControllerMessenger, + getRootControllerMessenger, +} from './test-utils'; +import type { SnapResponseWithInterface } from './types'; describe('handleRequest', () => { it('sends a JSON-RPC request and returns the response', async () => { @@ -36,7 +39,7 @@ describe('handleRequest', () => { `, }); - const snap = await handleInstallSnap(snapId); + const snap = await installSnap(snapId); const response = await handleRequest({ ...snap, handler: HandlerType.OnRpcRequest, @@ -58,6 +61,66 @@ describe('handleRequest', () => { await snap.executionService.terminateAllSnaps(); }); + it('gets an interface from the Snap', async () => { + const { snapId, close: closeServer } = await getMockServer({ + manifest: getSnapManifest({ + initialPermissions: { + // eslint-disable-next-line @typescript-eslint/naming-convention + snap_dialog: {}, + }, + }), + sourceCode: ` + module.exports.onRpcRequest = async () => { + return await snap.request({ + method: 'snap_dialog', + params: { + content: { + type: 'Text', + props: { + children: 'Hello, world!', + }, + key: null, + }, + }, + }); + } + `, + }); + + const snap = await installSnap(snapId); + const response = handleRequest({ + ...snap, + handler: HandlerType.OnRpcRequest, + request: { + method: 'foo', + params: ['bar'], + }, + }); + + const ui = await response.getInterface(); + + expect(ui).toStrictEqual({ + cancel: expect.any(Function), + clickElement: expect.any(Function), + content: { + type: 'Text', + props: { + children: 'Hello, world!', + }, + key: null, + }, + ok: expect.any(Function), + selectFromRadioGroup: expect.any(Function), + selectFromSelector: expect.any(Function), + selectInDropdown: expect.any(Function), + typeInField: expect.any(Function), + uploadFile: expect.any(Function), + }); + + await closeServer(); + await snap.executionService.terminateAllSnaps(); + }); + it('can get an interface from the SnapInterfaceController if the result contains an id', async () => { const controllerMessenger = getRootControllerMessenger(); @@ -81,7 +144,7 @@ describe('handleRequest', () => { port: 4242, }); - const snap = await handleInstallSnap(snapId); + const snap = await installSnap(snapId); const response = await handleRequest({ ...snap, controllerMessenger, @@ -118,7 +181,7 @@ describe('handleRequest', () => { port: 4242, }); - const snap = await handleInstallSnap(snapId); + const snap = await installSnap(snapId); const response = await handleRequest({ ...snap, controllerMessenger, @@ -162,7 +225,7 @@ describe('handleRequest', () => { port: 4242, }); - const snap = await handleInstallSnap(snapId); + const snap = await installSnap(snapId); const promise = handleRequest({ ...snap, controllerMessenger, @@ -189,7 +252,7 @@ describe('handleRequest', () => { `, }); - const snap = await handleInstallSnap(snapId); + const snap = await installSnap(snapId); const response = await handleRequest({ ...snap, handler: HandlerType.OnRpcRequest, diff --git a/packages/snaps-jest/src/internals/request.ts b/packages/snaps-simulation/src/request.ts similarity index 95% rename from packages/snaps-jest/src/internals/request.ts rename to packages/snaps-simulation/src/request.ts index 95f8e6d808..1436fbcfe6 100644 --- a/packages/snaps-jest/src/internals/request.ts +++ b/packages/snaps-simulation/src/request.ts @@ -16,20 +16,16 @@ import { } from '@metamask/utils'; import { nanoid } from '@reduxjs/toolkit'; +import type { RootControllerMessenger } from './controllers'; +import { getInterface, getInterfaceActions } from './interface'; +import { clearNotifications, getNotifications } from './store'; +import type { RunSagaFunction, Store } from './store'; +import { SnapResponseStruct } from './structs'; import type { RequestOptions, SnapHandlerInterface, SnapRequest, -} from '../types'; -import type { RunSagaFunction, Store } from './simulation'; -import { - clearNotifications, - getInterface, - getInterfaceActions, - getNotifications, -} from './simulation'; -import type { RootControllerMessenger } from './simulation/controllers'; -import { SnapResponseStruct } from './structs'; +} from './types'; export type HandleRequestOptions = { snapId: SnapId; diff --git a/packages/snaps-jest/src/internals/simulation/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts similarity index 95% rename from packages/snaps-jest/src/internals/simulation/simulation.test.ts rename to packages/snaps-simulation/src/simulation.test.ts index 5482742032..f8187bf894 100644 --- a/packages/snaps-jest/src/internals/simulation/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -10,20 +10,20 @@ import { AuxiliaryFileEncoding, text } from '@metamask/snaps-sdk'; import { VirtualFile } from '@metamask/snaps-utils'; import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; +import { DEFAULT_SRP } from './constants'; +import { getHooks, installSnap, registerActions } from './simulation'; +import { createStore, setInterface } from './store'; import { getMockOptions, getMockServer, getRestrictedSnapInterfaceControllerMessenger, getRootControllerMessenger, -} from '../../test-utils'; -import { DEFAULT_SRP } from './constants'; -import { getHooks, handleInstallSnap, registerActions } from './simulation'; -import { createStore, setInterface } from './store'; +} from './test-utils'; describe('handleInstallSnap', () => { it('installs a Snap and returns the execution service', async () => { const { snapId, close } = await getMockServer(); - const installedSnap = await handleInstallSnap(snapId); + const installedSnap = await installSnap(snapId); expect(installedSnap.executionService).toBeInstanceOf( NodeThreadExecutionService, @@ -280,6 +280,14 @@ describe('registerActions', () => { const { runSaga, store } = createStore(getMockOptions()); const controllerMessenger = getRootControllerMessenger(false); + it('registers `PhishingController:maybeUpdateState`', async () => { + registerActions(controllerMessenger, runSaga); + + expect( + await controllerMessenger.call('PhishingController:maybeUpdateState'), + ).toBeUndefined(); + }); + it('registers `PhishingController:testOrigin`', async () => { registerActions(controllerMessenger, runSaga); diff --git a/packages/snaps-jest/src/internals/simulation/simulation.ts b/packages/snaps-simulation/src/simulation.ts similarity index 99% rename from packages/snaps-jest/src/internals/simulation/simulation.ts rename to packages/snaps-simulation/src/simulation.ts index bf6420c037..6d83ac424e 100644 --- a/packages/snaps-jest/src/internals/simulation/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -134,7 +134,7 @@ export type MiddlewareHooks = { * @returns The installed Snap object. * @template Service - The type of the execution service. */ -export async function handleInstallSnap< +export async function installSnap< Service extends new (...args: any[]) => InstanceType< typeof AbstractExecutionService >, diff --git a/packages/snaps-jest/src/internals/simulation/store/index.ts b/packages/snaps-simulation/src/store/index.ts similarity index 80% rename from packages/snaps-jest/src/internals/simulation/store/index.ts rename to packages/snaps-simulation/src/store/index.ts index 56dd5612fb..4ec0adc667 100644 --- a/packages/snaps-jest/src/internals/simulation/store/index.ts +++ b/packages/snaps-simulation/src/store/index.ts @@ -1,3 +1,4 @@ +export * from './mocks'; export * from './notifications'; export * from './state'; export * from './store'; diff --git a/packages/snaps-jest/src/internals/simulation/store/mocks.test.ts b/packages/snaps-simulation/src/store/mocks.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/mocks.test.ts rename to packages/snaps-simulation/src/store/mocks.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/mocks.ts b/packages/snaps-simulation/src/store/mocks.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/mocks.ts rename to packages/snaps-simulation/src/store/mocks.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/notifications.test.ts b/packages/snaps-simulation/src/store/notifications.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/notifications.test.ts rename to packages/snaps-simulation/src/store/notifications.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/notifications.ts b/packages/snaps-simulation/src/store/notifications.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/notifications.ts rename to packages/snaps-simulation/src/store/notifications.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/state.test.ts b/packages/snaps-simulation/src/store/state.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/state.test.ts rename to packages/snaps-simulation/src/store/state.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/state.ts b/packages/snaps-simulation/src/store/state.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/state.ts rename to packages/snaps-simulation/src/store/state.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/store.test.ts b/packages/snaps-simulation/src/store/store.test.ts similarity index 96% rename from packages/snaps-jest/src/internals/simulation/store/store.test.ts rename to packages/snaps-simulation/src/store/store.test.ts index 974b77bdea..76b4f5484e 100644 --- a/packages/snaps-jest/src/internals/simulation/store/store.test.ts +++ b/packages/snaps-simulation/src/store/store.test.ts @@ -1,4 +1,4 @@ -import { getMockOptions } from '../../../test-utils'; +import { getMockOptions } from '../test-utils'; import { createStore } from './store'; describe('createStore', () => { diff --git a/packages/snaps-jest/src/internals/simulation/store/store.ts b/packages/snaps-simulation/src/store/store.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/store.ts rename to packages/snaps-simulation/src/store/store.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/ui.test.ts b/packages/snaps-simulation/src/store/ui.test.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/ui.test.ts rename to packages/snaps-simulation/src/store/ui.test.ts diff --git a/packages/snaps-jest/src/internals/simulation/store/ui.ts b/packages/snaps-simulation/src/store/ui.ts similarity index 100% rename from packages/snaps-jest/src/internals/simulation/store/ui.ts rename to packages/snaps-simulation/src/store/ui.ts diff --git a/packages/snaps-jest/src/internals/structs.test.tsx b/packages/snaps-simulation/src/structs.test.tsx similarity index 100% rename from packages/snaps-jest/src/internals/structs.test.tsx rename to packages/snaps-simulation/src/structs.test.tsx diff --git a/packages/snaps-jest/src/internals/structs.ts b/packages/snaps-simulation/src/structs.ts similarity index 100% rename from packages/snaps-jest/src/internals/structs.ts rename to packages/snaps-simulation/src/structs.ts diff --git a/packages/snaps-simulation/src/test-utils/controller.ts b/packages/snaps-simulation/src/test-utils/controller.ts new file mode 100644 index 0000000000..6913a30eb6 --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/controller.ts @@ -0,0 +1,63 @@ +import { PhishingDetectorResultType } from '@metamask/phishing-controller'; +import type { SnapInterfaceControllerAllowedActions } from '@metamask/snaps-controllers'; +import { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; + +import type { RootControllerAllowedActions } from '../controllers'; + +export const getRootControllerMessenger = (mocked = true) => { + const messenger = new MockControllerMessenger< + RootControllerAllowedActions, + any + >(); + + if (mocked) { + messenger.registerActionHandler( + 'PhishingController:maybeUpdateState', + async () => Promise.resolve(), + ); + + messenger.registerActionHandler('PhishingController:testOrigin', () => ({ + result: false, + type: PhishingDetectorResultType.All, + })); + + messenger.registerActionHandler( + 'ExecutionService:handleRpcRequest', + jest.fn(), + ); + + messenger.registerActionHandler( + 'ApprovalController:hasRequest', + () => true, + ); + + messenger.registerActionHandler( + 'ApprovalController:acceptRequest', + async (_id: string, value: unknown) => ({ value }), + ); + } + + return messenger; +}; + +export const getRestrictedSnapInterfaceControllerMessenger = ( + messenger: ReturnType< + typeof getRootControllerMessenger + > = getRootControllerMessenger(), +) => { + const snapInterfaceControllerMessenger = messenger.getRestricted< + 'SnapInterfaceController', + SnapInterfaceControllerAllowedActions['type'] + >({ + name: 'SnapInterfaceController', + allowedActions: [ + 'PhishingController:testOrigin', + 'PhishingController:maybeUpdateState', + 'ApprovalController:hasRequest', + 'ApprovalController:acceptRequest', + ], + allowedEvents: [], + }); + + return snapInterfaceControllerMessenger; +}; diff --git a/packages/snaps-simulation/src/test-utils/index.ts b/packages/snaps-simulation/src/test-utils/index.ts new file mode 100644 index 0000000000..9567e012a3 --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/index.ts @@ -0,0 +1,3 @@ +export * from './controller'; +export * from './options'; +export * from './server'; diff --git a/packages/snaps-jest/src/test-utils/options.ts b/packages/snaps-simulation/src/test-utils/options.ts similarity index 88% rename from packages/snaps-jest/src/test-utils/options.ts rename to packages/snaps-simulation/src/test-utils/options.ts index c0d48e94fe..54f9dbe927 100644 --- a/packages/snaps-jest/src/test-utils/options.ts +++ b/packages/snaps-simulation/src/test-utils/options.ts @@ -1,5 +1,5 @@ -import { DEFAULT_SRP } from '../internals'; -import type { SimulationOptions } from '../internals'; +import { DEFAULT_SRP } from '../constants'; +import type { SimulationOptions } from '../options'; /** * Get the options for the simulation. diff --git a/packages/snaps-simulation/src/test-utils/server.ts b/packages/snaps-simulation/src/test-utils/server.ts new file mode 100644 index 0000000000..2bc66a857b --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/server.ts @@ -0,0 +1,164 @@ +import type { SnapId } from '@metamask/snaps-sdk'; +import type { + LocalizationFile, + SnapManifest, + VirtualFile, +} from '@metamask/snaps-utils'; +import { + DEFAULT_SNAP_BUNDLE, + getMockSnapFilesWithUpdatedChecksum, + getSnapManifest, +} from '@metamask/snaps-utils/test-utils'; +import type { Application } from 'express'; +import express from 'express'; +import type { Server } from 'http'; +import type { AddressInfo } from 'net'; + +export type MockServerOptions = { + /** + * The source code to serve. This is assumed to be a string of JavaScript + * code. + */ + sourceCode?: string; + + /** + * The snap manifest to serve. Defaults to the result of calling + * {@link getSnapManifest}. This can be used to serve a modified manifest. + * + * The checksum in the manifest will be updated to match the source code. + */ + manifest?: SnapManifest; + + /** + * The port to listen on. Defaults to `0`, which means that the OS will + * choose a random available port. + */ + port?: number; + + /** + * Auxiliary files to serve. + */ + auxiliaryFiles?: VirtualFile[]; + + /** + * Localization files to serve. + */ + localizationFiles?: { + path: string; + file: LocalizationFile; + }[]; +}; + +/** + * Get a mock server that serves the given source code, and manifest. + * + * The server will listen on a random available port. The returned object + * contains the Snap ID of the server, as well as a `close` method that can be + * used to close the server. + * + * @param options - The options to use. + * @param options.sourceCode - The source code to serve. This is assumed to be a + * string of JavaScript code. + * @param options.manifest - The snap manifest to serve. Defaults to the result + * of calling {@link getSnapManifest}. This can be used to serve a modified + * manifest. + * @param options.port - The port to listen on. Defaults to `0`, which means + * that the OS will choose a random available port. + * @param options.auxiliaryFiles - Auxiliary files to serve. + * @param options.localizationFiles - Localization files to serve. + * @returns The mock server. + */ +export async function getMockServer({ + sourceCode = DEFAULT_SNAP_BUNDLE, + manifest = getSnapManifest({ + initialPermissions: {}, + }), + auxiliaryFiles = [], + localizationFiles = [], + port = 0, +}: MockServerOptions = {}) { + const snapFiles = await getMockSnapFilesWithUpdatedChecksum({ + manifest, + sourceCode, + auxiliaryFiles, + localizationFiles: localizationFiles.map(({ file }) => file), + }); + + const app = express(); + app.use('/snap.manifest.json', (_, response) => { + response.end(snapFiles.manifest.value); + }); + + app.use('/package.json', (_, response) => { + response.end(snapFiles.packageJson.value); + }); + + app.use( + `/${snapFiles.manifest.result.source.location.npm.filePath}`, + (_, response) => { + response.end(snapFiles.sourceCode.value); + }, + ); + + const icon = snapFiles.svgIcon; + if (icon) { + app.use( + `/${snapFiles.manifest.result.source.location.npm.iconPath}`, + (_, response) => { + response.end(icon.value); + }, + ); + } + + auxiliaryFiles?.forEach((file) => { + app.use(`/${file.path}`, (_, response) => { + response.end(file.value.toString()); + }); + }); + + localizationFiles?.forEach(({ file, path }) => { + app.use(`/${path}`, (_, response) => { + response.end(JSON.stringify(file)); + }); + }); + + const server = await listen(app, port); + const address = server.address() as AddressInfo; + const snapId = `local:http://localhost:${address.port}` as SnapId; + + return { + snapId, + close: async () => close(server), + }; +} + +/** + * Listen on the given port. This function is a wrapper around `app.listen`, but + * it returns a promise rather than accepting a callback. + * + * @param app - The Express app to listen on. + * @param port - The port to listen on. + * @returns A promise that resolves to the server. + */ +async function listen(app: Application, port: number) { + return new Promise((resolve) => { + const server = app.listen(port, () => { + resolve(server); + }); + }); +} + +/** + * Close the given server. This function is a wrapper around `server.close`, but + * it returns a promise rather than accepting a callback. + * + * @param server - The server to close. + * @returns A promise that resolves when the server is closed. + */ +async function close(server: Server) { + return new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }); +} diff --git a/packages/snaps-simulation/src/test-utils/snap/invalid-snap/snap.manifest.json b/packages/snaps-simulation/src/test-utils/snap/invalid-snap/snap.manifest.json new file mode 100644 index 0000000000..d329dd36eb --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/snap/invalid-snap/snap.manifest.json @@ -0,0 +1,17 @@ +{ + "proposedName": "bar", + "description": "baz", + "version": "1.0.0", + "source": { + "shasum": "D3ANeNZ7C1Ynx0GTP07afj72Jq06Srlq49QZkhICY+E=", + "location": { + "npm": { + "filePath": "snap.js", + "packageName": "qux", + "registry": "https://registry.npmjs.org" + } + } + }, + "initialPermissions": {}, + "manifestVersion": "0.1" +} diff --git a/packages/snaps-simulation/src/test-utils/snap/package.json b/packages/snaps-simulation/src/test-utils/snap/package.json new file mode 100644 index 0000000000..7387132749 --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/snap/package.json @@ -0,0 +1,4 @@ +{ + "name": "qux", + "version": "1.0.0" +} diff --git a/packages/snaps-simulation/src/test-utils/snap/snap.js b/packages/snaps-simulation/src/test-utils/snap/snap.js new file mode 100644 index 0000000000..a033fc24c7 --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/snap/snap.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-console +console.log('Hello, world!'); + +module.exports.onRpcRequest = () => null; diff --git a/packages/snaps-simulation/src/test-utils/snap/snap.manifest.json b/packages/snaps-simulation/src/test-utils/snap/snap.manifest.json new file mode 100644 index 0000000000..e2dc9f902b --- /dev/null +++ b/packages/snaps-simulation/src/test-utils/snap/snap.manifest.json @@ -0,0 +1,17 @@ +{ + "proposedName": "bar", + "description": "baz", + "version": "1.0.0", + "source": { + "shasum": "uaLwMO39qzKbshqPM6W2Ju7gkO/czuwgNKpjzXRXJj0=", + "location": { + "npm": { + "filePath": "snap.js", + "packageName": "qux", + "registry": "https://registry.npmjs.org" + } + } + }, + "initialPermissions": {}, + "manifestVersion": "0.1" +} diff --git a/packages/snaps-simulation/src/types.ts b/packages/snaps-simulation/src/types.ts new file mode 100644 index 0000000000..8678308634 --- /dev/null +++ b/packages/snaps-simulation/src/types.ts @@ -0,0 +1,472 @@ +import type { NotificationType, EnumToUnion } from '@metamask/snaps-sdk'; +import type { JSXElement } from '@metamask/snaps-sdk/jsx'; +import type { InferMatching } from '@metamask/snaps-utils'; +import type { Infer } from '@metamask/superstruct'; +import type { Json, JsonRpcId, JsonRpcParams } from '@metamask/utils'; + +import type { + SignatureOptionsStruct, + SnapOptionsStruct, + SnapResponseStruct, + TransactionOptionsStruct, +} from './structs'; + +export type RequestOptions = { + /** + * The JSON-RPC request ID. + */ + id?: JsonRpcId; + + /** + * The JSON-RPC method. + */ + method: string; + + /** + * The JSON-RPC params. + */ + params?: JsonRpcParams; + + /** + * The origin to send the request from. + */ + origin?: string; +}; + +/** + * The `runCronjob` options. This is the same as {@link RequestOptions}, except + * that it does not have an `origin` property. + */ +export type CronjobOptions = Omit; + +/** + * The options to use for transaction requests. + * + * @property chainId - The CAIP-2 chain ID to send the transaction on. Defaults + * to `eip155:1`. + * @property origin - The origin to send the transaction from. Defaults to + * `metamask.io`. + * @property from - The address to send the transaction from. Defaults to a + * randomly generated address. + * @property to - The address to send the transaction to. Defaults to a randomly + * generated address. + * @property value - The value to send with the transaction. Defaults to `0`. + * @property data - The data to send with the transaction. Defaults to `0x`. + * @property gasLimit - The gas limit to use for the transaction. Defaults to + * `21_000`. + * @property maxFeePerGas - The maximum fee per gas to use for the transaction. + * Defaults to `1`. + * @property maxPriorityFeePerGas - The maximum priority fee per gas to use for + * the transaction. Defaults to `1`. + * @property nonce - The nonce to use for the transaction. Defaults to `0`. + */ +export type TransactionOptions = Infer; + +/** + * The options to use for signature requests. + * + * @property origin - The origin to send the signature request from. Defaults to + * `metamask.io`. + * @property from - The address to send the signature from. Defaults to a + * randomly generated address. + * @property data - The data to sign. Defaults to `0x`. + * @property signatureMethod - The signature method. + */ +export type SignatureOptions = Infer; + +/** + * The options to use for requests to the snap. + * + * @property timeout - The timeout in milliseconds to use. Defaults to `1000`. + */ +export type SnapOptions = Infer; + +/** + * Options for uploading a file. + * + * @property fileName - The name of the file. By default, this is inferred from + * the file path if it's a path, and defaults to an empty string if it's a + * `Uint8Array`. + * @property contentType - The content type of the file. By default, this is + * inferred from the file name if it's a path, and defaults to + * `application/octet-stream` if it's a `Uint8Array` or the content type cannot + * be inferred from the file name. + */ +export type FileOptions = { + fileName?: string; + contentType?: string; +}; + +export type SnapInterfaceActions = { + /** + * Click on an interface element. + * + * @param name - The element name to click. + */ + clickElement(name: string): Promise; + + /** + * Type a value in a interface field. + * + * @param name - The element name to type in. + * @param value - The value to type. + */ + typeInField(name: string, value: string): Promise; + + /** + * Select an option with a value in a dropdown. + * + * @param name - The element name to type in. + * @param value - The value to type. + */ + selectInDropdown(name: string, value: string): Promise; + + /** + * Choose an option with a value from radio group. + * + * @param name - The element name to type in. + * @param value - The value to type. + */ + selectFromRadioGroup(name: string, value: string): Promise; + + /** + * Choose an option with a value from Selector component. + * + * @param name - The element name to type in. + * @param value - The value to type. + */ + selectFromSelector(name: string, value: string): Promise; + + /** + * Upload a file. + * + * @param name - The element name to upload the file to. + * @param file - The file to upload. This can be a path to a file or a + * `Uint8Array` containing the file contents. If this is a path, the file is + * resolved relative to the current working directory. + * @param options - The file options. + * @param options.fileName - The name of the file. By default, this is + * inferred from the file path if it's a path, and defaults to an empty string + * if it's a `Uint8Array`. + * @param options.contentType - The content type of the file. By default, this + * is inferred from the file name if it's a path, and defaults to + * `application/octet-stream` if it's a `Uint8Array` or the content type + * cannot be inferred from the file name. + */ + uploadFile( + name: string, + file: string | Uint8Array, + options?: FileOptions, + ): Promise; +}; + +/** + * A `snap_dialog` alert interface. + */ +export type SnapAlertInterface = { + /** + * The type of the interface. This is always `alert`. + */ + type: 'alert'; + + /** + * The content to show in the alert. + */ + content: JSXElement; + + /** + * Close the alert. + */ + ok(): Promise; +}; + +/** + * A `snap_dialog` confirmation interface. + */ +export type SnapConfirmationInterface = { + /** + * The type of the interface. This is always `confirmation`. + */ + type: 'confirmation'; + + /** + * The content to show in the confirmation. + */ + content: JSXElement; + + /** + * Close the confirmation. + */ + ok(): Promise; + + /** + * Cancel the confirmation. + */ + cancel(): Promise; +}; + +/** + * A `snap_dialog` prompt interface. + */ +export type SnapPromptInterface = { + /** + * The type of the interface. This is always `prompt`. + */ + type: 'prompt'; + + /** + * The content to show in the prompt. + */ + content: JSXElement; + + /** + * Close the prompt. + * + * @param value - The value to close the prompt with. + */ + ok(value?: string): Promise; + + /** + * Cancel the prompt. + */ + cancel(): Promise; +}; + +/** + * A `snap_dialog` default interface that has a Footer with two buttons defined. + * The approval of this confirmation is handled by the snap. + */ +export type DefaultSnapInterfaceWithFooter = { + /** + * The content to show in the interface. + */ + content: JSXElement; +}; + +/** + * A `snap_dialog` default interface that has a Footer with one button defined. + * A cancel button is automatically applied to the interface in this case. + */ +export type DefaultSnapInterfaceWithPartialFooter = + DefaultSnapInterfaceWithFooter & { + /** + * Cancel the dialog. + */ + cancel(): Promise; + }; + +/** + * A `snap_dialog` default interface that has no Footer defined. + * A cancel and ok button is automatically applied to the interface in this case. + */ +export type DefaultSnapInterfaceWithoutFooter = + DefaultSnapInterfaceWithPartialFooter & { + /** + * Close the dialog. + * + */ + ok(): Promise; + }; + +export type DefaultSnapInterface = + | DefaultSnapInterfaceWithFooter + | DefaultSnapInterfaceWithPartialFooter + | DefaultSnapInterfaceWithoutFooter; + +export type SnapInterface = ( + | SnapAlertInterface + | SnapConfirmationInterface + | SnapPromptInterface + | DefaultSnapInterface +) & + SnapInterfaceActions; + +export type SnapRequestObject = { + /** + * Get a user interface object from a snap. This will throw an error if the + * snap does not show a user interface within the timeout. + * + * @param options - The options to use. + * @param options.timeout - The timeout in milliseconds to use. Defaults to + * `1000`. + * @returns The user interface object. + */ + getInterface(options?: SnapOptions): Promise; +}; + +/** + * A pending request object. This is a promise with extra + * {@link SnapRequestObject} fields. + */ +export type SnapRequest = Promise & SnapRequestObject; + +/** + * The options to use for mocking a JSON-RPC request. + */ +export type JsonRpcMockOptions = { + /** + * The JSON-RPC request method. + */ + method: string; + + /** + * The JSON-RPC response, which will be returned when a request with the + * specified method is sent. + */ + result: Json; +}; + +/** + * This is the main entry point to interact with the snap. It is returned by + * {@link installSnap}, and has methods to send requests to the snap. + * + * @example + * import { installSnap } from '@metamask/snaps-jest'; + * + * const snap = await installSnap(); + * const response = await snap.request({ method: 'hello' }); + * + * expect(response).toRespondWith('Hello, world!'); + */ +export type Snap = { + /** + * Send a JSON-RPC request to the snap. + * + * @param request - The request. This is similar to a JSON-RPC request, but + * has an extra `origin` field. + * @returns The response promise, with extra {@link SnapRequestObject} fields. + */ + request(request: RequestOptions): SnapRequest; + + /** + * Send a transaction to the snap. + * + * @param transaction - The transaction. This is similar to an Ethereum + * transaction object, but has an extra `origin` field. Any missing fields + * will be filled in with default values. + * @returns The response. + */ + onTransaction( + transaction?: Partial, + ): Promise; + + /** + * Send a transaction to the snap. + * + * @param transaction - The transaction. This is similar to an Ethereum + * transaction object, but has an extra `origin` field. Any missing fields + * will be filled in with default values. + * @returns The response. + * @deprecated Use {@link onTransaction} instead. + */ + sendTransaction( + transaction?: Partial, + ): Promise; + + /** + * Send a signature request to the snap. + * + * @param signature - The signature request object. Contains the params from + * the various signature methods, but has an extra `origin` and `signatureMethod` field. + * Any missing fields will be filled in with default values. + * @returns The response. + */ + onSignature( + signature?: Partial, + ): Promise; + + /** + * Run a cronjob in the snap. This is similar to {@link request}, but the + * request will be sent to the `onCronjob` method of the snap. + * + * @param cronjob - The cronjob request. This is similar to a JSON-RPC + * request, and is normally specified in the snap manifest, under the + * `endowment:cronjob` permission. + * @returns The response promise, with extra {@link SnapRequestObject} fields. + */ + onCronjob(cronjob?: Partial): SnapRequest; + + /** + * Run a cronjob in the snap. This is similar to {@link request}, but the + * request will be sent to the `onCronjob` method of the snap. + * + * @param cronjob - The cronjob request. This is similar to a JSON-RPC + * request, and is normally specified in the snap manifest, under the + * `endowment:cronjob` permission. + * @returns The response promise, with extra {@link SnapRequestObject} fields. + * @deprecated Use {@link onCronjob} instead. + */ + runCronjob(cronjob: CronjobOptions): SnapRequest; + + /** + * Get the response from the snap's `onHomePage` method. + * + * @returns The response. + */ + onHomePage(): Promise; + + /** + * Mock a JSON-RPC request. This will cause the snap to respond with the + * specified response when a request with the specified method is sent. + * + * @param mock - The mock options. + * @param mock.method - The JSON-RPC request method. + * @param mock.result - The JSON-RPC response, which will be returned when a + * request with the specified method is sent. + * @example + * import { installSnap } from '@metamask/snaps-jest'; + * + * // In the test + * const snap = await installSnap(); + * snap.mockJsonRpc({ method: 'eth_accounts', result: ['0x1234'] }); + * + * // In the Snap + * const response = + * await ethereum.request({ method: 'eth_accounts' }); // ['0x1234'] + */ + mockJsonRpc(mock: JsonRpcMockOptions): { + /** + * Remove the mock. + */ + unmock(): void; + }; + + /** + * Close the page running the snap. This is mainly useful for cleaning up + * the test environment, and calling it is not strictly necessary. + * + * @returns A promise that resolves when the page is closed. + * @deprecated Snaps are now automatically closed when the test ends. This + * method will be removed in a future release. + */ + close(): Promise; +}; + +export type SnapHandlerInterface = { + content: JSXElement; +} & SnapInterfaceActions; + +export type SnapResponseWithInterface = { + id: string; + response: { result: Json } | { error: Json }; + notifications: { + id: string; + message: string; + type: EnumToUnion; + }[]; + getInterface(): SnapHandlerInterface; +}; + +export type SnapResponseWithoutInterface = Omit< + SnapResponseWithInterface, + 'getInterface' +>; + +export type SnapResponseType = + | SnapResponseWithoutInterface + | SnapResponseWithInterface; + +export type SnapResponse = InferMatching< + typeof SnapResponseStruct, + SnapResponseType +>; diff --git a/packages/snaps-simulation/src/validation.ts b/packages/snaps-simulation/src/validation.ts new file mode 100644 index 0000000000..d0564ff3bd --- /dev/null +++ b/packages/snaps-simulation/src/validation.ts @@ -0,0 +1,100 @@ +import { DialogType } from '@metamask/snaps-sdk'; +import type { FooterElement } from '@metamask/snaps-sdk/jsx'; +import { getJsxChildren } from '@metamask/snaps-utils'; +import { assert, hasProperty } from '@metamask/utils'; + +import { getElementByType } from './interface'; +import type { + DefaultSnapInterface, + DefaultSnapInterfaceWithFooter, + DefaultSnapInterfaceWithoutFooter, + DefaultSnapInterfaceWithPartialFooter, + SnapAlertInterface, + SnapConfirmationInterface, + SnapInterface, + SnapInterfaceActions, + SnapPromptInterface, +} from './types'; + +/** + * Ensure that the actual interface is an alert dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsAlertDialog( + ui: SnapInterface, +): asserts ui is SnapAlertInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Alert); +} + +/** + * Ensure that the actual interface is a confirmation dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsConfirmationDialog( + ui: SnapInterface, +): asserts ui is SnapConfirmationInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Confirmation); +} + +/** + * Ensure that the actual interface is a Prompt dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsPromptDialog( + ui: SnapInterface, +): asserts ui is SnapPromptInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Prompt); +} + +/** + * Ensure that the actual interface is a custom dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsCustomDialog( + ui: SnapInterface, +): asserts ui is DefaultSnapInterface & SnapInterfaceActions { + assert(!hasProperty(ui, 'type')); +} + +/** + * Ensure that the actual interface is a custom dialog with a complete footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(footer && getJsxChildren(footer).length === 2); +} + +/** + * Ensure that the actual interface is a custom dialog with a partial footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasPartialFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithPartialFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(footer && getJsxChildren(footer).length === 1); +} + +/** + * Ensure that the actual interface is a custom dialog without a footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasNoFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithoutFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(!footer); +} diff --git a/packages/snaps-simulation/tsconfig.build.json b/packages/snaps-simulation/tsconfig.build.json new file mode 100644 index 0000000000..e19a4c36e4 --- /dev/null +++ b/packages/snaps-simulation/tsconfig.build.json @@ -0,0 +1,33 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src"], + "exclude": [ + "**/*.test.ts", + "**/*.test.tsx", + "./src/**/test-utils", + "./src/**/__mocks__", + "./src/**/__snapshots__" + ], + "references": [ + { + "path": "../snaps-controllers/tsconfig.build.json" + }, + { + "path": "../snaps-execution-environments/tsconfig.build.json" + }, + { + "path": "../snaps-rpc-methods/tsconfig.build.json" + }, + { + "path": "../snaps-sdk/tsconfig.build.json" + }, + { + "path": "../snaps-utils/tsconfig.build.json" + } + ] +} diff --git a/packages/snaps-simulation/tsconfig.json b/packages/snaps-simulation/tsconfig.json new file mode 100644 index 0000000000..6fc0e7ff52 --- /dev/null +++ b/packages/snaps-simulation/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "jsx": "react-jsx", + "jsxImportSource": "@metamask/snaps-sdk" + }, + "include": ["./src"], + "references": [ + { + "path": "../snaps-controllers" + }, + { + "path": "../snaps-execution-environments" + }, + { + "path": "../snaps-rpc-methods" + }, + { + "path": "../snaps-sdk" + }, + { + "path": "../snaps-utils" + } + ] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 3236d1d92c..a3ba02417d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -10,6 +10,7 @@ { "path": "./packages/snaps-rollup-plugin/tsconfig.build.json" }, { "path": "./packages/snaps-rpc-methods/tsconfig.build.json" }, { "path": "./packages/snaps-sdk/tsconfig.build.json" }, + { "path": "./packages/snaps-simulation/tsconfig.build.json" }, { "path": "./packages/snaps-utils/tsconfig.build.json" }, { "path": "./packages/snaps-webpack-plugin/tsconfig.build.json" } ] diff --git a/tsconfig.json b/tsconfig.json index b65dc62c29..905ecb440a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ { "path": "./packages/snaps-rollup-plugin" }, { "path": "./packages/snaps-rpc-methods" }, { "path": "./packages/snaps-sdk" }, + { "path": "./packages/snaps-simulation" }, { "path": "./packages/snaps-utils" }, { "path": "./packages/snaps-webpack-plugin" } ], diff --git a/yarn.lock b/yarn.lock index af3d6368d0..1cb8c9d9b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5784,30 +5784,20 @@ __metadata: "@jest/types": "npm:^29.6.3" "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^6.0.2" "@metamask/eslint-config": "npm:^12.1.0" "@metamask/eslint-config-jest": "npm:^12.1.0" "@metamask/eslint-config-nodejs": "npm:^12.1.0" "@metamask/eslint-config-typescript": "npm:^12.1.0" - "@metamask/eth-json-rpc-middleware": "npm:^14.0.0" - "@metamask/json-rpc-engine": "npm:^9.0.2" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.2" - "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/phishing-controller": "npm:^12.0.2" "@metamask/snaps-controllers": "workspace:^" - "@metamask/snaps-execution-environments": "workspace:^" - "@metamask/snaps-rpc-methods": "workspace:^" "@metamask/snaps-sdk": "workspace:^" + "@metamask/snaps-simulation": "workspace:^" "@metamask/snaps-utils": "workspace:^" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - "@reduxjs/toolkit": "npm:^1.9.5" "@swc/core": "npm:1.3.78" "@swc/jest": "npm:^0.2.26" "@ts-bridge/cli": "npm:^0.5.1" "@types/jest": "npm:^27.5.1" - "@types/mime": "npm:^3.0.0" "@types/semver": "npm:^7.5.0" "@typescript-eslint/eslint-plugin": "npm:^5.42.1" "@typescript-eslint/parser": "npm:^6.21.0" @@ -5826,12 +5816,9 @@ __metadata: jest-environment-node: "npm:^29.5.0" jest-it-up: "npm:^2.0.0" jest-matcher-utils: "npm:^29.5.0" - mime: "npm:^3.0.0" prettier: "npm:^2.7.1" prettier-plugin-packagejson: "npm:^2.2.11" - readable-stream: "npm:^3.6.2" redux: "npm:^4.2.1" - redux-saga: "npm:^1.2.3" typescript: "npm:~5.3.3" languageName: unknown linkType: soft @@ -5969,6 +5956,60 @@ __metadata: languageName: unknown linkType: soft +"@metamask/snaps-simulation@workspace:^, @metamask/snaps-simulation@workspace:packages/snaps-simulation": + version: 0.0.0-use.local + resolution: "@metamask/snaps-simulation@workspace:packages/snaps-simulation" + dependencies: + "@lavamoat/allow-scripts": "npm:^3.0.4" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^6.0.2" + "@metamask/eslint-config": "npm:^12.1.0" + "@metamask/eslint-config-jest": "npm:^12.1.0" + "@metamask/eslint-config-nodejs": "npm:^12.1.0" + "@metamask/eslint-config-typescript": "npm:^12.1.0" + "@metamask/eth-json-rpc-middleware": "npm:^14.0.0" + "@metamask/json-rpc-engine": "npm:^9.0.2" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.2" + "@metamask/key-tree": "npm:^9.1.2" + "@metamask/permission-controller": "npm:^11.0.0" + "@metamask/phishing-controller": "npm:^12.0.2" + "@metamask/snaps-controllers": "workspace:^" + "@metamask/snaps-rpc-methods": "workspace:^" + "@metamask/snaps-sdk": "workspace:^" + "@metamask/snaps-utils": "workspace:^" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.2.1" + "@reduxjs/toolkit": "npm:^1.9.5" + "@ts-bridge/cli": "npm:^0.5.1" + "@types/express": "npm:^4.17.17" + "@types/jest": "npm:^27.5.1" + "@types/mime": "npm:^3.0.0" + "@types/readable-stream": "npm:^4.0.15" + "@typescript-eslint/eslint-plugin": "npm:^5.42.1" + "@typescript-eslint/parser": "npm:^6.21.0" + deepmerge: "npm:^4.2.2" + depcheck: "npm:^1.4.7" + eslint: "npm:^8.27.0" + eslint-config-prettier: "npm:^8.5.0" + eslint-plugin-import: "npm:^2.26.0" + eslint-plugin-jest: "npm:^27.1.5" + eslint-plugin-jsdoc: "npm:^41.1.2" + eslint-plugin-n: "npm:^15.7.0" + eslint-plugin-prettier: "npm:^4.2.1" + eslint-plugin-promise: "npm:^6.1.1" + express: "npm:^4.18.2" + jest: "npm:^29.0.2" + jest-it-up: "npm:^2.0.0" + mime: "npm:^3.0.0" + prettier: "npm:^2.7.1" + prettier-plugin-packagejson: "npm:^2.2.11" + readable-stream: "npm:^3.6.2" + redux-saga: "npm:^1.2.3" + ts-jest: "npm:^29.1.1" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/snaps-simulator@workspace:packages/snaps-simulator": version: 0.0.0-use.local resolution: "@metamask/snaps-simulator@workspace:packages/snaps-simulator" @@ -7534,14 +7575,14 @@ __metadata: linkType: hard "@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.17": - version: 4.17.17 - resolution: "@types/express@npm:4.17.17" + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" "@types/serve-static": "npm:*" - checksum: 10/e2959a5fecdc53f8a524891a16e66dfc330ee0519e89c2579893179db686e10cfa6079a68e0fb8fd00eedbcaf3eabfd10916461939f3bc02ef671d848532c37e + checksum: 10/7a6d26cf6f43d3151caf4fec66ea11c9d23166e4f3102edfe45a94170654a54ea08cf3103d26b3928d7ebcc24162c90488e33986b7e3a5f8941225edd5eb18c7 languageName: node linkType: hard