Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Jest expect types #2308

Merged
merged 1 commit into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions packages/snaps-jest/src/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface, @typescript-eslint/no-unused-vars, @typescript-eslint/no-namespace */

import type {
Component,
EnumToUnion,
NotificationType,
} from '@metamask/snaps-sdk';

interface SnapsMatchers {
/**
* Assert that the response is a JSON-RPC response with the given result. This
* is equivalent to calling `expect(response.result).toStrictEqual(result)`.
*
* @param response - The expected response result.
* @throws If the response is not a JSON-RPC response with the given result.
* @example
* const response = await request({ method: 'foo' });
* expect(response).toRespondWith('bar');
*/
toRespondWith(response: unknown): void;

/**
* Assert that the response is a JSON-RPC response with the given error. This
* is equivalent to calling `expect(response.error).toStrictEqual(error)`.
*
* @param error - The expected response error.
* @throws If the response is not a JSON-RPC response with the given error.
* @example
* const response = await request({ method: 'foo' });
* expect(response).toRespondWithError({
* code: -32601,
* message: 'The method does not exist / is not available.',
* stack: expect.any(String),
* data: { method: 'foo', cause: null },
* });
*/
toRespondWithError(error: unknown): void;

/**
* Assert that the Snap sent a notification with the expected message, and
* optionally the expected notification type. This is equivalent to calling
* `expect(response.notifications).toContainEqual({ message, type })`.
*
* @param message - The expected notification message.
* @param type - The expected notification type, i.e., 'inApp' or 'native'. If
* not provided, the type will be ignored.
* @throws If the snap did not send a notification with the expected message.
* @example
* const response = await request({ method: 'foo' });
* expect(response).toSendNotification('bar', NotificationType.InApp);
*/
toSendNotification(
message: string,
type?: EnumToUnion<NotificationType>,
): void;

/**
* Assert that the Snap rendered the expected component. This is equivalent to
* calling `expect(interface.content).toStrictEqual(component)`.
*
* @param component - The expected rendered component.
* @throws If the snap did not render the expected component.
* @example
* const response = request({ method: 'foo' });
* const ui = await response.getInterface();
* expect(ui).toRender(panel([heading('Hello, world!')]));
*/
toRender(component: Component): void;
}

// Extend the `expect` interface with the new matchers. This is used when
// importing `expect` from `@jest/globals`.
declare module 'expect' {
interface AsymmetricMatchers extends SnapsMatchers {}

// Ideally we would use `Matchers<Result>` instead of `Matchers<R>`, but
// TypeScript doesn't allow this:
// TS2428: All declarations of 'Matchers' must have identical type parameters.
interface Matchers<R> extends SnapsMatchers {}
}

// Extend the Jest global namespace with the new matchers. This is used when
// using the global `expect` function.
declare global {
namespace jest {
// Ideally we would use `Matchers<Result>` instead of `Matchers<R>`, but
// TypeScript doesn't allow this:
// TS2428: All declarations of 'Matchers' must have identical type parameters.
interface Matchers<R> extends SnapsMatchers {}
}
}
3 changes: 3 additions & 0 deletions packages/snaps-jest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line import/no-unassigned-import
import './global';

export { default, default as TestEnvironment } from './environment';
export * from './helpers';
export * from './options';
Expand Down
34 changes: 1 addition & 33 deletions packages/snaps-jest/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
NotificationType,
EnumToUnion,
Component,
} from '@metamask/snaps-sdk';
import type { Component } from '@metamask/snaps-sdk';
import type { Json, JsonRpcId, JsonRpcParams } from '@metamask/utils';
import type { Infer } from 'superstruct';

Expand All @@ -13,34 +9,6 @@ import type {
TransactionOptionsStruct,
} from './internals';

/* eslint-disable @typescript-eslint/consistent-type-definitions */
declare module 'expect' {
interface AsymmetricMatchers {
toRespondWith(response: unknown): void;
toRespondWithError(error: unknown): void;
toSendNotification(
message: string,
type?: EnumToUnion<NotificationType>,
): void;
toRender(component: Component): void;
}

// Ideally we would use `Matchers<Result>` instead of `Matchers<R>`, but
// TypeScript doesn't allow this:
// TS2428: All declarations of 'Matchers' must have identical type parameters.
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Matchers<R> {
toRespondWith(response: unknown): R;
toRespondWithError(error: unknown): R;
toSendNotification(
message: string,
type?: EnumToUnion<NotificationType>,
): R;
toRender(component: Component): R;
}
}
/* eslint-enable @typescript-eslint/consistent-type-definitions */

export type RequestOptions = {
/**
* The JSON-RPC request ID.
Expand Down
Loading