npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mix-n-matchers

v1.5.0

Published

Miscellaneous custom Jest matchers

Downloads

957

Readme

mix-n-matchers

Miscellaneous custom Jest matchers

Installation

Install via npm or yarn:

npm install -D mix-n-matchers
yarn add -D mix-n-matchers

Setup

Create a setup script with the following:

// add all matchers
import * as mixNMatchers from "mix-n-matchers";
expect.extend(mixNMatchers);

// or just add specific matchers
import {
  toBeCalledWithContext,
  lastCalledWithContext,
  nthCalledWithContext,
  exactly,
} from "mix-n-matchers";

expect.extend({
  toBeCalledWithContext,
  lastCalledWithContext,
  nthCalledWithContext,
  exactly,
});

Add your setup script to your Jest setupFilesAfterEnv configuration. For reference

"jest": {
  "setupFilesAfterEnv": ["./testSetup.js"]
}

To automatically extend expect with all matchers, you can use

"jest": {
  "setupFilesAfterEnv": ["mix-n-matchers/all"]
}

If you're using @jest/globals instead of injecting globals, you should use the jest-globals entry point instead of all.

"jest": {
  "setupFilesAfterEnv": ["mix-n-matchers/jest-globals"]
}

If you're using Vitest, you should instead use mix-n-matchers/vitest:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["mix-n-matchers/vitest"],
    include: ["src/**/*.vi.test.ts"],
  },
});

Typescript

If your editor does not recognise the custom mix-n-matchers matchers, add a global.d.ts file to your project with:

// global.d.ts
import "mix-n-matchers/all";

If you want finer control of which matchers are included (i.e. you're only extending with some), you can set this up yourself:

// global.d.ts
import type { MixNMatchers, AsymmetricMixNMatchers } from "mix-n-matchers";

declare global {
  namespace jest {
    export interface Matchers<R, T>
      extends Pick<
        MixNMatchers<R, T>,
        | "toBeCalledWithContext"
        | "lastCalledWithContext"
        | "nthCalledWithContext"
      > {}

    export interface Expect extends Pick<AsymmetricMixNMatchers, "exactly"> {}

    export interface InverseAsymmetricMatchers
      extends Pick<AsymmetricMixNMatchers, "exactly"> {}
  }
}

One method of ensuring this is in line with your actual setup file would be by exporting objects:

// testSetup.js
import {
  toBeCalledWithContext,
  lastCalledWithContext,
  nthCalledWithContext,
  exactly,
} from "mix-n-matchers";

export const mixNMatchers = {
  toBeCalledWithContext,
  lastCalledWithContext,
  nthCalledWithContext,
};

expect.extend(mixNMatchers);

export const asymmMixNMatchers = { exactly };

expect.extend(asymmMixNMatchers);
// global.d.ts
import type { mixNMatchers, asymmMixNMatchers } from "../testSetup.js";
import type { MixNMatchers, AsymmetricMixNMatchers } from "mix-n-matchers";

declare global {
  namespace jest {
    export interface Matchers<R, T>
      extends Pick<MixNMatchers<R, T>, keyof typeof mixNMatchers> {}

    export interface Expect
      extends Pick<AsymmetricMixNMatchers, keyof typeof asymmMixNMatchers> {}

    export interface InverseAsymmetricMatchers
      extends Pick<AsymmetricMixNMatchers, keyof typeof asymmMixNMatchers> {}
  }
}

@jest/globals

If you disable injectGlobals for Jest and instead import from '@jest/globals', the setup will look slightly different.

If you just want all of the matchers, your global.d.ts file should have:

// global.d.ts
import "mix-n-matchers/jest-globals";

If you want finer control over which matchers are added, you should follow the below:

// global.d.ts
import type { MixNMatchers, AsymmetricMixNMatchers } from "mix-n-matchers";

declare module "@jest/extend" {
  export interface Matchers<R, T>
    extends Pick<
      MixNMatchers<R, T>,
      "toBeCalledWithContext" | "lastCalledWithContext" | "nthCalledWithContext"
    > {}

  export interface AsymmetricMatchers
    extends Pick<AsymmetricMixNMatchers, "exactly"> {}
}

Vitest

If you're using Vitest, you should use mix-n-matchers/vitest instead of the 'all' entry point:

// global.d.ts
import "mix-n-matchers/vitest";

If you want finer control over which matchers are added, you should follow the below:

// global.d.ts
import { expect } from "vitest";
import type { MixNMatchers, AsymmetricMixNMatchers } from "mix-n-matchers";

declare module "vitest" {
  interface Assertion<T>
    extends Pick<
      mixNMatchers.MixNMatchers<void, T>,
      "toBeCalledWithContext" | "lastCalledWithContext" | "nthCalledWithContext"
    > {}
  interface AsymmetricMatchersContaining
    extends Pick<mixNMatchers.AsymmetricMixNMatchers, "exactly"> {}
}

Matchers

Asymmetric Matchers

exactly

Uses Object.is to ensure referential equality in situations where deep equality would typically be used.

expect(mock).toBeCalledWith(expect.exactly(reference));

typeOf

Checks equality using typeof.

expect(mock).toBeCalledWith(expect.typeOf("string"));

oneOf

Checks that the value matches one of the specified values, using deep equality.

expect(mock).toBeCalledWith(expect.oneOf([1, 2, 3]));

ofEnum / enum

Checks that the value is a member of the specified enum.

Exported as ofEnum and aliased as enum in auto-setup files. (See Tips)

expect(mock).toBeCalledWith(expect.ofEnum(MyEnum));
expect(mock).toBeCalledWith(expect.enum(MyEnum));

arrayContainingOnly

Checks that value is an array only containing the specified values. Values can be repeated (or omitted), but all elements present should be present in the expected array.

Put another way, it checks that the received array is a subset of (or equal to) the expected array. This is in contrast to arrayContaining, which checks that the received array is a superset of (or equal to) the expected array.

// will pass
expect({ array: [1, 2] }).toEqual({
  array: expect.arrayContainingOnly([1, 2, 3]),
});
expect({ array: [1, 2] }).toEqual({
  array: expect.arrayContainingOnly([1, 2]),
});
expect({ array: [1, 1] }).toEqual({
  array: expect.arrayContainingOnly([1, 2, 2]),
});
// will fail
expect({ array: [1, 2, 3] }).toEqual({
  array: expect.arrayContainingOnly([1, 2]),
});

objectContainingOnly

Checks that value is an object only containing the specified keys. Keys can be omitted, but all keys present should match the expected object.

Put another way, it checks that the received object is a subset of (or equal to) the expected object. This is in contrast to objectContaining, which checks that the received object is a superset of (or equal to) the expected object.

// will pass
expect({ a: 1 }).toEqual(expect.objectContainingOnly({ a: 1, b: 2 }));
expect({ a: 1, b: 2 }).toEqual(expect.objectContainingOnly({ a: 1, b: 2 }));
// will fail
expect({ a: 1, b: 2 }).toEqual(expect.objectContainingOnly({ a: 1 }));

sequence

Matches an iterable that satisfies the specified sequence of predicates.

expect({
  array: [1, 2, 3],
}).toEqual({
  array: expect.sequence(
    (x) => x === 1,
    (x) => x === 2,
    (x) => x === 3,
  ),
});

sequenceOf

Matches an iterable that satisfies the specified sequence of values, using deep equality.

expect({
  array: [1, 2, 3],
}).toEqual({
  array: expect.sequenceOf(1, 2, 3),
});

strictSequenceOf

Matches an iterable that satisfies the specified sequence of values, using strict deep equality.

expect({
  array: [1, 2, 3],
}).toEqual({
  array: expect.strictSequenceOf(1, 2, 3),
});

iterableOf

Matches an iterable where every value matches the expected value, using deep equality.

expect({
  array: [1, 2, 3],
}).toEqual({
  array: expect.iterableOf(expect.any(Number)),
});

strictIterableOf

Matches an iterable where every value matches the expected value, using strict deep equality.

expect({
  array: [1, 2, 3],
}).toEqual({
  array: expect.strictIterableOf(expect.any(Number)),
});

recordOf

Matches an object where every value matches the expected value, using deep equality.

Optionally, you can pass two arguments and the first will be matched against keys.

Note: keys and values are retrieved using Object.entries, so only string (non-symbol) enumerable keys are checked.

expect({
  object: { a: 1, b: 2 },
}).toEqual({
  object: expect.recordOf(expect.any(Number)),
});

expect({
  object: { a: 1, b: 2 },
}).toEqual({
  object: expect.recordOf(expect.any(String), expect.any(Number)),
});

strictRecordOf

Matches an object where every value matches the expected value, using strict deep equality.

Optionally, you can pass two arguments and the first will be matched against keys.

Note: keys and values are retrieved using Object.entries, so only string (non-symbol) enumerable keys are checked.

expect({
  object: { a: 1, b: 2 },
}).toEqual({
  object: expect.strictRecordOf(expect.any(Number)),
});

expect({
  object: { a: 1, b: 2 },
}).toEqual({
  object: expect.strictRecordOf(expect.any(String), expect.any(Number)),
});

Symmetric Matchers

toBeEnum

Assert a value is a member of the specified enum.

expect(getDirection()).toBeEnum(Direction);

toSatisfySequence

Assert a value is an iterable that satisfies the specified sequence of predicates.

expect([1, 2, 3]).toSatisfySequence(
  (x) => x === 1,
  (x) => x === 2,
  (x) => x === 3,
);

toEqualSequence

Assert a value is an iterable that satisfies the specified sequence of values, using deep equality.

expect([1, 2, 3]).toEqualSequence(1, 2, 3);

toStrictEqualSequence

Assert a value is an iterable that satisfies the specified sequence of values, using strict deep equality.

expect([1, 2, 3]).toStrictEqualSequence(1, 2, 3);

toBeIterableOf

Assert a value is an iterable where every value matches the expected value, using deep equality.

expect([1, 2, 3]).toBeIterableOf(expect.any(Number));

tobeStrictIterableOf

Assert a value is an iterable where every value matches the expected value, using strict deep equality.

expect([1, 2, 3]).toBeStrictIterableOf(expect.any(Number));

toBeRecordOf

Assert a value is an object where every value matches the expected value, using deep equality.

Optionally, you can pass two arguments and the first will be matched against keys.

Note: keys and values are retrieved using Object.entries, so only string (non-symbol) enumerable keys are checked.

expect({ a: 1, b: 2 }).toBeRecordOf(expect.any(Number));
expect({ a: 1, b: 2 }).toBeRecordOf(expect.any(String), expect.any(Number));

toBeStrictRecordOf

Assert a value is an object where every value matches the expected value, using strict deep equality.

Optionally, you can pass two arguments and the first will be matched against keys.

Note: keys and values are retrieved using Object.entries, so only string (non-symbol) enumerable keys are checked.

expect({ a: 1, b: 2 }).toBeStrictRecordOf(expect.any(Number));
expect({ a: 1, b: 2 }).toBeStrictRecordOf(
  expect.any(String),
  expect.any(Number),
);

toBeCalledWithContext/toHaveBeenCalledWithContext

Assert a function has been called with a specific context (this).

expect(mock).toBeCalledWithContext(expectedContext);
expect(mock).toHaveBeenCalledWithContext(expectedContext);

lastCalledWithContext/toHaveBeenLastCalledWithContext

Assert the last call of a function was with a specific context (this).

expect(mock).lastCalledWithContext(expectedContext);
expect(mock).toHaveBeenLastCalledWithContext(expectedContext);

nthCalledWithContext/toHaveBeenNthCalledWithContext

Assert the Nth call of a function was with a specific context (this).

expect(mock).nthCalledWithContext(1, expectedContext);
expect(mock).toHaveBeenNthCalledWithContext(1, expectedContext);

Tips

Aliasing expect.ofEnum to expect.enum

As enum is a reserved word in Javascript, it is not possible to export a matcher with this name. However, you can alias it in your setup file:

import { ofEnum } from "mix-n-matchers";

expect.extend({ enum: ofEnum });

To add this to your global.d.ts:

// global.d.ts
import type { AsymmetricMixNMatchers } from "mix-n-matchers";

declare global {
  namespace jest {
    export interface Expect {
      enum: AsymmetricMixNMatchers["ofEnum"];
    }

    interface InverseAsymmetricMatchers {
      enum: AsymmetricMixNMatchers["ofEnum"];
    }
  }
}

This approach can be adapted for Jest globals and Vitest as well.

After this setup, you should be able to use expect.enum as a matcher.

expect(mock).toBeCalledWith(expect.enum(MyEnum));

This is automatically done for you with the auto-setup files (mix-n-matchers/all, mix-n-matchers/jest-globals, mix-n-matchers/vitest).

Asymmetric Matchers vs Symmetric Matchers

When expect.extend is called, each matcher is added as both an asymmetric and symmetric matcher.

expect.extend({
  foo(received) {
    const pass = received === "foo";
    return {
      pass,
      message: pass ? () => "Expected 'foo'" : () => "Expected not 'foo'",
    };
  },
});

expect(value).foo(); // symmetric

expect(value).toEqual(expect.foo()); // asymmetric

However, conventionally there is a difference in how these matchers are named. For example, expect().toBeAnArray vs expect.array.

mix-n-matchers intentionally only exposes types for matchers as either asymmetric or symmetric, and not both. Sometimes a matcher is available as both, but with different names. For example, expect().toBeEnum and expect.ofEnum.

This helps to avoid confusion and makes it clear which matchers are designed to be asymmetric and which are symmetric.

If there's any existing matchers that are only available as asymmetric matchers and you'd like to use them as symmetric matchers (or vice versa), please open an issue or a pull request!

You can of course choose to expose these types yourself to enable both symmetric and asymmetric usage of a matcher.

declare module "mix-n-matchers" {
  interface MixNMatchers<R, T> extends Pick<AsymmetricMixNMatchers, "typeOf"> {}
  interface AsymmetricMixNMatchers
    extends Pick<MixNMatchers, "toBeCalledWithContext"> {}
}

// now allowed
expect(value).typeOf("string");
expect(value).toEqual({ fn: expect.toBeCalledWithContext(context) });