Node TAP 18.7.2

tap plugins

A plugin is a package that adds functionality to node-tap by exporting special values on known named exports. It can do so in any or all of the following ways.

All of the fields are optional, but at least one of plugin, loader, or config must be exported.

Managing Plugins#

The easiest way to manage plugins is by using the tap plugin command. This handles all of the installation, building, updating the configs, and so on.

You can also manually specify plugins by updating the plugin config value in .taprc or package.json, or on the command line.

By default, built in plugins are always present unless they are explicitly excluded. To exclude a built in plugin in the configuration, prefix its name with a ! character.

For example, if you run tap plugin rm @tapjs/typescript, then your .taprc file will be updated to:

plugin:
  - "!@tapjs/typescript"

Rebuilding the Test Class#

The Test class must be regenerated on every plugin change. This is done automatically by the cli whenever tests are run, but you can also explicitly rebuild by running tap build at any time.

plugin#

The plugin export, if defined, must be a function that takes a TestBase object as its first argument, and optionally an options object as its second argument. The return value is an object. Any properties or methods of that object will appear on the Test class that is provided to test suites.

If an options object is accepted as the second parameter, it must extend the BaseOpts interface, and all properties should be optional. (Technically it can have required fields, but this means that all tests have to provide an options object when using your plugin, which is kind of annoying.)

For example, a very simple "hello, world" plugin:

import { TapPlugin, TestBase } from '@tapjs/core'
export interface HelloSayer {
  hello: (who?: string) => string
}
export const plugin: TapPlugin<HelloSayer> = (t: TestBase) => {
  return {
    hello: (who: string = 'world') => {
      console.error(`${t.name} says "Hello, ${who}!"`)
    }
  }
}

The returned object can be anything. Methods on the object will be called in the this-context of the returned object itself. If you need a reference to the original TestBase object, you can stash a reference to it when loading.

After all of the plugins have finished loading, a .t member will be added to the TestBase object, which is the fully plugged-in Test proxy.

loader#

If a loader string is exported, then it will be used when spawning test processes.

This is how, for example, the @tapjs/typescript plugin adds support for TypeScript test files.

If preload = true is exported, then the loader will be added ahead of non-preload loaders. This is necessary when the loader needs to be attached early, as in the case of loading different JavaScript dialects.

importLoader#

The --loader API is experimental, and replaced by --import and Module.register starting at Node.js version 20.6.

If a plugin exports an importLoader string, then this will be added to the test arguments with --import instead of the --loader argument.

On Node versions that do not support --import, the --loader argument will be used instead if the plugin exports a loader string.

See the Node.js ES Module Hooks documentation for more information.

config#

The config export allows a plugin to define configuration values that will be parsed from the command line or from tap config files.

The object must be a value that can be passed to jackspeak's Jack#addFields method. For example, the @tapjs/snapshot plugin exports a snapshot config, which tells it to save new snapshots rather than compare against stored snapshots.

config[field].nodeArgs Function#

If a config field has a nodeArgs method, then this method will be called with the current value of the configuration field, and is expected to return an array of strings, which will be added to the execArgv of the test process.

This is provided primarily for interoperability with node:test tests, so that for example --grep can be translated to --test-name-pattern. However, it's also a way for a plugin to modify the arguments passed to node, ahead of the main test script file.

Do not use this to add --loader or --import arguments. Those should be done using the loader or importLoader exports, respectively.

testFileExtensions#

If an array of strings is exported as testFileExtensions, then this will be added to the set of file types that tap loads by default.

For example, the @tapjs/typescript plugin uses this to advertise that it makes tap able to load ts and jsx files.

Generally, you'd do this when also adding a loader, since adding file types that node doesn't already know how to run won't work.

Plugin Requirements#

Plugin Collisions#

The first plugin in a list that provides a given method or property will be the one that "wins", as far as the object presented in test code is concerned.

However, within a given plugin, it only sees itself and the TestBase object it's been given. For example, if returning an object constructed from a class defined in the plugin, this will refer to that object, always.

// first-plugin
export const plugin = (t: TestBase) => {
  return {
    // this is the first plugin to register this value
    // so this is what shows up on the Test object
    myVal: 4,
    getFirstPluginVal() {
      return this.myVal // always returns 4
    },
    // this is the first plugin to register this method
    // so this is what shows up on the Test object
    getFour() {
      return 4
    },
  }
}
// second-plugin
export const plugin = (t: TestBase) => {
  return {
    // user will never see this, because first-plugin registered it
    myVal: 5,
    getSecondPluginValue() {
      return this.myVal // always returns 5
    },
    // overridden, this isn't the 'getFour' that the user will see
    getFour() {
      return 'four'
    },
  }
}

Then in the test:

import t from 'tap'
console.log(t.myVal) // 4, not 5
console.log(t.getFour()) // 4, not 'four'
console.log(t.getFirstPluginVal()) // 4
console.log(t.getSecondPluginVal()) // 5