Skip to content

Provide an extensible API for implementing custom matchers #23

@allansson

Description

@allansson

I have an idea for an extensible approach of implementing matchers, which I think would help the current code while providing a mechanism for implementing custom matchers.

// mod.ts

// If colorization is turned off, then the colorize function would just be the identity function `(value) => value`
type MessageFormatter = (colorize: (string: value, color: AnsiColor) => string) => FormattedMessage

export interface Fail {
  message: MessageFormatter
}

export interface Expectation {
  readonly failed;
  fail(): void
}

export type MatcherFn<
  Actual = unknown,
  Expected extends unknown[] = unknown[],
  R extends Promise<void> | void = Promise<void> | void,
> = (
  expectation: Expectation,
  actual: Actual,
  ...expected: Expected
) => R;

export type ToExpectFn<Matcher> = Matcher extends
  MatcherFn<infer _Actual, infer Expected, infer Return>
  ? (...expected: Expected) => Return
  : never;

  /**
   * This interface is a registry of all available matchers. Implementations should
   * extend this interface via declaration merging.
   */
export interface Matchers<Actual> {}

/**
 * This type extracts all of the matchers that can be used on values of type `Actual`.
 * For instance, it allows us to hide `toBeVisible` unless the `Actual` type is a `Locator`.
 */
export type ApplicableMatchers<Actual> = {
  [
    P in keyof Matchers<Actual> as Matchers<Actual>[P] extends MatcherFn<Actual, any, any> ? P
      : never
  ]: ToExpectFn<Matchers<Actual>>;
};

export interface ExpectFunction {
  expect: <T>(actual: T, message?: string) => ApplicableMatchers<T>;
}

export interface GlobalExpectFunction {
  /**
   * This function is used to register a new matcher. Since `Name` is a keyof the
   * `Matchers<T>` interface, it will be checked at compile time that the matcher
   * has actually been added to the `Matchers<T>` interface.
   */
  register<Name extends keyof Matchers<any>>(
    name: Name,
    matcher: Matchers<any>[Name],
  ): void;
}

The implementation of a matcher (internally) would be something like:

import { expect, type Expectation } from "./mod.ts"

declare module "./mod.ts" {
  export interface Matchers<T> {
    toEqual(expectation: Expectation, actual: T, expected: AsymmetricMatch<T>)
  }
}

expect.register("toEqual", (expectation, actual, expected) => {
  if (!equal(actual, expected)) {
    expectation.fail({
      message: createObjectDiffFormatter(actual, expected)
    })
  }
})

Capturing the execution context, rendering the message, etc. would be handled identically across matchers inside expect, so there would be less duplication. A retrying matcher would just be any matcher that used the withRetry utility:

// mod.ts
function withRetry<Actual, Expected>(fn: MatcherFn<Actual, Expected, any>): Matcher<Actual, Expected, Promise<void>> {
  return async (expectation, actual, expected) => {
    // Retry `fn` with a new `Expectation` each time and if we're out of retries, fail the `expectation` with the last error from `fn`
  }
}


// toHaveText.ts
import { expect, withRetry } from "./mod.ts"

declare module "./mod.ts" {
  export interface Matchers<T> {
    // Just an example of how a conditional type can be used to hide a function based on the `Actual` type.
    toHaveText: T extends Locator ? (expectation: Expectation, actual: Locator, expected: AsymmetricMatch<T>) : never
  }
}

expect.register("toEqual", withRetry((expectation, actual, expected) => {
  if (!equal(actual, expected)) {
    expectation.fail({
      message: createObjectDiffFormatter(actual, expected)
    })
  }
}))

I'm not sure if the extensibility is even needed, but I think it would improve our code as well. Each matcher can be written and tested in isolation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions