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#
- Plugins must be able to be loaded both with
require()
andimport()
. - Plugins must export at least one of
loader
,plugin
, orconfig
. - Plugins should be written in TypeScript, or failing that, provide complete type information so that users' tests can infer the types of added properties and methods appropriately.
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