@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
JavaScript
;
// 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