UNPKG

@metamask/snaps-jest

Version:

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

291 lines 14.5 kB
"use strict"; // Note: Because this file imports from `@jest/globals`, it can only be used in // a Jest environment. This is why it's not exported from the index file. Object.defineProperty(exports, "__esModule", { value: true }); exports.toTrace = exports.toTrackEvent = exports.toTrackError = exports.toRender = exports.toSendNotification = exports.toRespondWithError = exports.toRespondWith = void 0; const globals_1 = require("@jest/globals"); const jsx_1 = require("@metamask/snaps-sdk/jsx"); const snaps_simulation_1 = require("@metamask/snaps-simulation"); const snaps_utils_1 = require("@metamask/snaps-utils"); const superstruct_1 = require("@metamask/superstruct"); const utils_1 = require("@metamask/utils"); const jest_matcher_utils_1 = require("jest-matcher-utils"); /** * Ensure that the actual value is a response from the `request` function. * * @param actual - The actual value. * @param matcherName - The name of the matcher. * @param options - The matcher options. */ function assertActualIsSnapResponse(actual, matcherName, options) { if (!(0, superstruct_1.is)(actual, snaps_simulation_1.SnapResponseStruct)) { throw new Error((0, jest_matcher_utils_1.matcherErrorMessage)((0, jest_matcher_utils_1.matcherHint)(matcherName, undefined, undefined, options), `${(0, jest_matcher_utils_1.RECEIVED_COLOR)('received')} value must be a response from the \`request\` function`, (0, jest_matcher_utils_1.printWithType)('Received', actual, jest_matcher_utils_1.printReceived))); } } /** * Ensure that the actual value is a response from the `request` function, and * that it has a `ui` property. * * @param actual - The actual value. * @param matcherName - The name of the matcher. * @param options - The matcher options. */ function assertHasInterface(actual, matcherName, options) { if (!(0, superstruct_1.is)(actual, snaps_simulation_1.InterfaceStruct) || !actual.content) { throw new Error((0, jest_matcher_utils_1.matcherErrorMessage)((0, jest_matcher_utils_1.matcherHint)(matcherName, undefined, undefined, options), `${(0, jest_matcher_utils_1.RECEIVED_COLOR)('received')} value must have a \`content\` property`, (0, jest_matcher_utils_1.printWithType)('Received', actual, jest_matcher_utils_1.printReceived))); } } /** * Check if a JSON-RPC response matches the expected value. This matcher is * intended to be used with the `expect` global. * * @param actual - The actual response. * @param expected - The expected response. * @returns The status and message. */ const toRespondWith = function (actual, expected) { assertActualIsSnapResponse(actual, 'toRespondWith'); const { response } = actual; if ((0, utils_1.hasProperty)(response, 'error')) { const message = () => `${this.utils.matcherHint('.toRespondWith')}\n\n` + `Expected response: ${this.utils.printExpected(expected)}\n` + `Received error: ${this.utils.printReceived(response.error)}`; return { message, pass: false }; } const pass = this.equals(response.result, expected); const message = pass ? () => `${this.utils.matcherHint('.not.toRespondWith')}\n\n` + `Expected: ${this.utils.printExpected(expected)}\n` + `Received: ${this.utils.printReceived(response.result)}` : () => `${this.utils.matcherHint('.toRespondWith')}\n\n` + `Expected: ${this.utils.printExpected(expected)}\n` + `Received: ${this.utils.printReceived(response.result)}`; return { message, pass }; }; exports.toRespondWith = toRespondWith; const toRespondWithError = function (actual, expected) { assertActualIsSnapResponse(actual, 'toRespondWithError'); const { response } = actual; if ((0, utils_1.hasProperty)(response, 'result')) { const message = () => `${this.utils.matcherHint('.toRespondWithError')}\n\n` + `Expected error: ${this.utils.printExpected(expected)}\n` + `Received result: ${this.utils.printReceived(response.result)}`; return { message, pass: false }; } const pass = this.equals(response.error, expected); const message = pass ? () => `${this.utils.matcherHint('.not.toRespondWithError')}\n\n` + `Expected: ${this.utils.printExpected(expected)}\n` + `Received: ${this.utils.printReceived(response.error)}` : () => `${this.utils.matcherHint('.toRespondWithError')}\n\n` + `Expected: ${this.utils.printExpected(expected)}\n` + `Received: ${this.utils.printReceived(response.error)}`; return { message, pass }; }; exports.toRespondWithError = toRespondWithError; /** * Check if the snap sent a notification with the expected message. This matcher * is intended to be used with the `expect` global. * * @param actual - The actual response. * @param expectedMessage - The expected notification message. * @param expectedType - The expected notification type. * @param expectedTitle - The expected notification title. * @param expectedContent - The expected notification JSX content. * @param expectedFooterLink - The expected footer link object. * @returns The status and message. */ const toSendNotification = function (actual, expectedMessage, expectedType, expectedTitle, expectedContent, expectedFooterLink) { assertActualIsSnapResponse(actual, 'toSendNotification'); const { notifications } = actual; let jsxContent; if ('getInterface' in actual) { jsxContent = actual.getInterface().content; } const notificationValidator = (notification) => { const { type, message, title, footerLink } = notification; if (!this.equals(message, expectedMessage)) { return false; } if (expectedType && type !== expectedType) { return false; } if (title && !this.equals(title, expectedTitle)) { return false; } if (jsxContent && !this.equals(jsxContent, expectedContent)) { return false; } if (footerLink && !this.equals(footerLink, expectedFooterLink)) { return false; } return true; }; const pass = notifications.some(notificationValidator); const transformedNotifications = notifications.map((notification) => { return { ...notification, // Ok to cast here as the function returns if the param is falsy content: (0, snaps_utils_1.serialiseJsx)(jsxContent), }; }); const message = () => { let testMessage = pass ? `${this.utils.matcherHint('.not.toSendNotification')}\n\n` : `${this.utils.matcherHint('.toSendNotification')}\n\n`; const { title, type, message: notifMessage, footerLink, content, } = transformedNotifications[0]; testMessage += `Expected message: ${this.utils.printExpected(expectedMessage)}\n`; if (expectedType) { testMessage += `Expected type: ${this.utils.printExpected(expectedType)}\n`; } if (title) { testMessage += `Expected title: ${this.utils.printExpected(expectedTitle)}\n`; // We want to check if the expected content is actually JSX content, otherwise `serialiseJsx` won't return something useful. if ((0, superstruct_1.is)(expectedContent, jsx_1.JSXElementStruct)) { testMessage += `Expected content: ${this.utils.printExpected((0, snaps_utils_1.serialiseJsx)(expectedContent))}\n`; } else { testMessage += `Expected content: ${this.utils.printExpected(expectedContent)}\n`; } } if (footerLink) { testMessage += `Expected footer link: ${this.utils.printExpected(expectedFooterLink)}\n`; } testMessage += `Received message: ${this.utils.printExpected(notifMessage)}\n`; if (expectedType) { testMessage += `Received type: ${this.utils.printReceived(type)}\n`; } if (title) { testMessage += `Received title: ${this.utils.printReceived(title)}\n`; testMessage += `Received content: ${this.utils.printReceived((0, snaps_utils_1.serialiseJsx)(content))}\n`; } if (footerLink) { testMessage += `Received footer link: ${this.utils.printReceived(footerLink)}\n`; } return testMessage; }; return { message, pass }; }; exports.toSendNotification = toSendNotification; const toRenderLegacy = function (actual, expected) { assertHasInterface(actual, 'toRender'); const { content } = actual; const expectedElement = (0, snaps_utils_1.getJsxElementFromComponent)(expected); const pass = this.equals(content, expectedElement); // This is typed as `string | null`, but in practice it's always a string. // The function only returns `null` if both the expected and actual values // are numbers, bigints, or booleans, which is never the case here. const difference = (0, jest_matcher_utils_1.diff)(expectedElement, content); const message = pass ? () => `${this.utils.matcherHint('.not.toRender')}\n\n` + `Expected:\n${this.utils.printExpected(expectedElement)}\n\n` + `Received:\n${this.utils.printReceived(content)}` + `\n\nDifference:\n\n${difference}` : () => `${this.utils.matcherHint('.toRender')}\n\n` + `Expected:\n${this.utils.printExpected(expectedElement)}\n\n` + `Received:\n${this.utils.printReceived(content)}` + `\n\nDifference:\n\n${difference}`; return { message, pass }; }; const toRender = // This should not return a promise. // eslint-disable-next-line @typescript-eslint/promise-function-async function (actual, expected) { assertHasInterface(actual, 'toRender'); if (!(0, jsx_1.isJSXElementUnsafe)(expected)) { return toRenderLegacy.call(this, actual, expected); } const { content } = actual; const pass = this.equals(content, expected); // This is typed as `string | null`, but in practice it's always a string. // The function only returns `null` if both the expected and actual values // are numbers, bigints, or booleans, which is never the case here. const difference = (0, jest_matcher_utils_1.diff)((0, snaps_utils_1.serialiseJsx)(expected), (0, snaps_utils_1.serialiseJsx)(content)); const message = pass ? () => `${this.utils.matcherHint('.not.toRender')}\n\n` + `Expected:\n${(0, jest_matcher_utils_1.EXPECTED_COLOR)((0, snaps_utils_1.serialiseJsx)(expected))}\n\n` + `Received:\n${(0, jest_matcher_utils_1.RECEIVED_COLOR)((0, snaps_utils_1.serialiseJsx)(content))}` + `\n\nDifference:\n\n${difference}` : () => `${this.utils.matcherHint('.toRender')}\n\n` + `Expected:\n${(0, jest_matcher_utils_1.EXPECTED_COLOR)((0, snaps_utils_1.serialiseJsx)(expected))}\n\n` + `Received:\n${(0, jest_matcher_utils_1.RECEIVED_COLOR)((0, snaps_utils_1.serialiseJsx)(content))}` + `\n\nDifference:\n\n${difference}`; return { message, pass }; }; exports.toRender = toRender; const toTrackError = function (actual, errorData) { assertActualIsSnapResponse(actual, 'toTrackError'); const errorValidator = (error) => { if (!errorData) { // If no error data is provided, we just check for the existence of an // error. return true; } return this.equals(error, errorData); }; const { errors } = actual.tracked; const pass = errors.some(errorValidator); const message = pass ? () => `${this.utils.matcherHint('.not.toTrackError')}\n\n` + `Expected not to track error with data: ${this.utils.printExpected(errorData)}\n` + `Received errors: ${this.utils.printReceived(errors)}` : () => `${this.utils.matcherHint('.toTrackError')}\n\n` + `Expected to track error with data: ${this.utils.printExpected(errorData)}\n` + `Received errors: ${this.utils.printReceived(errors)}`; return { message, pass }; }; exports.toTrackError = toTrackError; const toTrackEvent = function (actual, eventData) { assertActualIsSnapResponse(actual, 'toTrackEvent'); const eventValidator = (event) => { if (!eventData) { // If no event data is provided, we just check for the existence of an // event. return true; } return this.equals(event, eventData); }; const { events } = actual.tracked; const pass = events.some(eventValidator); const message = pass ? () => `${this.utils.matcherHint('.not.toTrackEvent')}\n\n` + `Expected not to track event with data: ${this.utils.printExpected(eventData)}\n` + `Received events: ${this.utils.printReceived(events)}` : () => `${this.utils.matcherHint('.toTrackEvent')}\n\n` + `Expected to track event with data: ${this.utils.printExpected(eventData)}\n` + `Received events: ${this.utils.printReceived(events)}`; return { message, pass }; }; exports.toTrackEvent = toTrackEvent; const toTrace = function (actual, traceData) { assertActualIsSnapResponse(actual, 'toTrace'); const traceValidator = (trace) => { if (!traceData) { // If no trace data is provided, we just check for the existence of a // trace. return true; } return this.equals(trace, traceData); }; const { traces } = actual.tracked; const pass = traces.some(traceValidator); const message = pass ? () => `${this.utils.matcherHint('.not.toTrace')}\n\n` + `Expected not to trace with data: ${this.utils.printExpected(traceData)}\n` + `Received traces: ${this.utils.printReceived(traces)}` : () => `${this.utils.matcherHint('.toTrace')}\n\n` + `Expected to trace with data: ${this.utils.printExpected(traceData)}\n` + `Received traces: ${this.utils.printReceived(traces)}`; return { message, pass }; }; exports.toTrace = toTrace; globals_1.expect.extend({ toRespondWith: exports.toRespondWith, toRespondWithError: exports.toRespondWithError, toSendNotification: exports.toSendNotification, toRender: exports.toRender, toTrackError: exports.toTrackError, toTrackEvent: exports.toTrackEvent, toTrace: exports.toTrace, }); //# sourceMappingURL=matchers.cjs.map