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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@giancosta86/rigoletto

v1.0.1

Published

Elegant matchers in TypeScript for Vitest

Readme

rigoletto

Elegant matchers in TypeScript for Vitest

NPM Version

Logo

Elegance in software development is the result of several aspects - primarily expressiveness and minimalism - not only in the main codebase of a project, but in its tests as well.

Consequently, in modern test frameworks like Vitest, reusing test logic via declarative custom matchers - such as expect(myShape).toBeConvex() - seems a very effective option... but alas, these constructs are not always perceived as easy to create, let alone to test extensively.

As a result, rigoletto focuses on:

  1. the creation and testing of custom matchers for Vitest, via a minimalist TypeScript programming interface.

  2. providing various sets of ready-made matchers - especially for vanilla TypeScript as well as NodeJS.

  3. as a plus, exporting configuration files to easily reference jest-extended in Vitest-based tests.

This guide will now briefly explain what rigoletto can bring to your project.

Installation

The package on NPM is:

@giancosta86/rigoletto

The public API entirely resides in multiple subpackages:

  • @giancosta86/rigoletto/creation: utilities for defining new matchers.

  • @giancosta86/rigoletto/jest-extended: ready-made jest-extended declarations and registrations.

  • @giancosta86/rigoletto/matchers/all: all the custom matchers provided by Rigoletto.

  • @giancosta86/rigoletto/matchers/nodejs: a gallery of matchers for NodeJS.

  • @giancosta86/rigoletto/matchers/vanilla: a gallery of matchers for any JavaScript VM.

  • @giancosta86/rigoletto/testing: utilities for testing new matchers using fluent notation.

Each subpackage should be referenced via its name, with no references to its modules.

Defining your own matchers

Creating a basic synchronous matcher

The most straightforward way to create a matcher function is implementBooleanMatcher(), from @giancosta86/rigoletto/creation, designed for matchers that simply check a boolean condition - that is, a vast majority.

More precisely, let's create a new matcher step by step:

  1. Define the matcher function:

    import type { ExpectationResult, MatcherState } from "@vitest/expect";
    
    export function toBeEven(
      this: MatcherState,
      subject: number
    ): ExpectationResult {
      //Implementation here
    }
  2. Add the implementation just by returning a call to implementBooleanMatcher()

    import type { ExpectationResult, MatcherState } from "@vitest/expect";
    import { implementBooleanMatcher } from "@giancosta86/rigoletto";
    
    export function toBeEven(
      this: MatcherState,
      subject: number
    ): ExpectationResult {
      return implementBooleanMatcher({
        matcherState: this,
        assertionCondition: subject % 2 == 0,
        errorWhenAssertionFails: `${subject} is odd!`,
        errorWhenNegationFails: `Unexpected even number: ${subject}`
      });
    }

To plug the matcher into Vitest - especially when using TypeScript - you'll need to:

  1. Declare the TypeScript extensions:

    import "vitest";
    
    interface MyMatchers {
      toBeEven: () => void;
    }
    
    declare module "vitest" {
      interface Assertion<T = any> extends MyMatchers {}
      interface AsymmetricMatchersContaining extends MyMatchers {}
    }
  2. Register the matcher into expect(), to make it available at runtime:

    import { expect } from "vitest";
    
    expect.extend({
      toBeEven
    });

Should you need a more sophisticated example regarding synchronous matchers - using the general-purpose implementMatcher() function - please refer to the toThrowClass matcher.

Creating an asynchronous matcher

Creating an asynchronous matcher is equally easy - in the case of implementBooleanMatcher() just pass a Promise<boolean> as its condition.

For example, let's walk through the implementation of the toExistInFileSystem() matcher - already provided by rigoletto:

  1. Define the matcher function:

    import type { ExpectationResult, MatcherState } from "@vitest/expect";
    
    export function toExistInFileSystem(
      this: MatcherState,
      subjectPath: string
    ): ExpectationResult {
      //Implementation goes here
    }
  2. Define or import an async function - or any other way to obtain a Promise:

    async function pathExists(path: string): Promise<boolean> {
      //Implementation here
    }
  3. Add the matcher implementation just by returning a call to implementBooleanMatcher() - passing the Promise as its assertion condition:

    import type { ExpectationResult, MatcherState } from "@vitest/expect";
    import { implementBooleanMatcher } from "@giancosta86/rigoletto";
    
    export function toExistInFileSystem(
      this: MatcherState,
      subjectPath: string
    ): ExpectationResult {
      return implementBooleanMatcher({
        matcherState: this,
        assertionCondition: pathExists(subjectPath),
        errorWhenAssertionFails: `Missing file system entry: '${subjectPath}'`,
        errorWhenNegationFails: `Unexpected file system entry: '${subjectPath}'`
      });
    }

And that's all! As you can notice, the result type of the matcher is always ExpectationResult - no matter whether it is synchronous or asynchronous.

The general-purpose implementMatcher() function also supports Promise in its flows - in particular, you can merely declare async functions among its inputs.

Once a matcher has been implemented, let's test it - because rigoletto supports that, too! 🥳

Testing matchers

The idea at the core of rigoletto's testing API - provided by @giancosta86/rigoletto/testing - resides in the fact that, given a scenario (for example, «when the input is an even number»), a matcher should ✅succeed(/❌fail) - and, conversely, its negation should ❌fail(/✅succeed).

To avoid code duplication, you can use the scenario() function - structurally equivalent to describe() - and its fluent notation; for example, in the case of the toBeEven() matcher declared previously, we could test this scenario:

import { scenario } from "@giancosta86/rigoletto/testing";

//We can build an arbitrary test structure
//using describe(), as usual
describe("toBeEven()", () => {
  describe("in its most basic form", () => {
    scenario("when applied to an even number")
      .subject(8)
      .passes(e => e.toBeEven())
      .withErrorWhenNegated("Unexpected even number: 8");
  });
});

The above scenario(), followed by ✅.pass(), actually expands into a describe() call with the same description, containing 2 tests:

  • one, containing expect(8).toBeEven(), which is expected to ✅pass

  • another, containing expect(8).not.toBeEven(), which is expected to ❌fail with the given error message

You can use as many scenarios as you wish - for example:

scenario("when applied to an odd number")
  .subject(13)
  .fails(e => e.toBeEven())
  .withError("13 is odd!");

In this case, scenario() followed by ❌.fail() expands into the following tests:

  • one, containing expect(13).toBeEven(), which is expected to ❌fail with the given error message

  • another, containing expect(13).not.toBeEven(), which is expected to ✅pass

It is interesting to note that scenario() transparently supports both synchronous and asynchronous matchers, with the very same notation.

Important note

When defining a scenario via the scenario() function, you must never use .not inside a .passes() or .fails() call: use the opposite function instead.

For example, in lieu of testing like this:

scenario("when applied to an odd number")
  .subject(13)
  .passes(e => e.not.toBeEven()) //WRONG!!! USE .fails() INSTEAD!
  .withError("13 is odd!");

use .fails(e => e.toBeEven()), as previously seen.

The gallery of matchers

rigoletto comes with several ready-made matchers - please, consult the subsections below for details.

Vanilla matchers

This is a gallery of matchers that can be called within any JavaScript VM supported by Vitest.

To use them, add this import to some .d.ts file referenced by tsconfig.json:

import "@giancosta86/rigoletto/matchers/vanilla";

In Vitest's configuration file, the following item must be included:

const config: ViteUserConfig = {
  test: {
    setupFiles: ["@giancosta86/rigoletto/matchers/vanilla"]
  }
};

NodeJS matchers

This is a gallery of matchers specifically designed for the NodeJS environment.

To use them, add this import to some .d.ts file referenced by tsconfig.json:

import "@giancosta86/rigoletto/matchers/nodejs";

In Vitest's configuration file, the following item must be included:

const config: ViteUserConfig = {
  test: {
    setupFiles: ["@giancosta86/rigoletto/matchers/nodejs"]
  }
};

Importing all matchers

This will import all the Rigoletto matchers described in the previous subsections - therefore, all the related requirements apply.

To reference them, add this import to some .d.ts file referenced by tsconfig.json:

import "@giancosta86/rigoletto/matchers/all";

In Vitest's configuration file, the following item must be included:

const config: ViteUserConfig = {
  test: {
    setupFiles: ["@giancosta86/rigoletto/matchers/all"]
  }
};

Using jest-extended

Rigoletto comes with support for jest-extended, simplifying its integration into test projects.

For TypeScript, just add the following import to some .d.ts file referenced by tsconfig.json:

import "@giancosta86/rigoletto/jest-extended";

In Vitest's configuration file, the following item must be included:

const config: ViteUserConfig = {
  test: {
    setupFiles: ["@giancosta86/rigoletto/jest-extended"]
  }
};

Trivia

The project name stems from the 🌷exquisite Italian 🎶opera «Rigoletto» by Giuseppe Verdi - whose protagonist, Rigoletto, is a court 🃏jester.

Further references

  • Vitest - Next Generation Testing Framework

  • TypeScript - JavaScript with syntax for types