babel-plugin-tester
Note
This documentation is for version 11.x of babel-plugin-tester. If you're on an
earlier version and interested in upgrading, check out the release notes for
tips. Discuss any undocumented breaks to backwards-compatibility or offer other
feedback here (or open a new issue). Documentation for earlier versions
can be found here.
Utilities for testing babel plugins and presets.
The Problem
You are writing a babel plugin or preset and want to write tests for
it too.
This Solution
This is a fairly simple abstraction to help you write tests for your babel
plugin or preset. It was built to work with Jest, but most of the
functionality will work with Mocha, Jasmine, node:test
,
Vitest, and any other test runner that defines standard describe
and
it
globals with async support (see appendix).
This package is tested on both Windows and nix (Ubuntu) environments.
Installation
This module is distributed via npm which is bundled with node and
should be installed as one of your project's devDependencies
:
npm install --save-dev babel-plugin-tester
Usage
Import
ESM:
import { pluginTester } from 'babel-plugin-tester';
CJS:
const { pluginTester } = require('babel-plugin-tester');
Default Export Compatibility
For backwards compatibility reasons, a default export is also available:
import pluginTester from 'babel-plugin-tester';
const pluginTester = require('babel-plugin-tester');
const pluginTester = require('babel-plugin-tester').default;
const { default: pluginTester } = require('babel-plugin-tester');
These default exports are considered deprecated and should be avoided.
They will be removed in the next major release.
Invoke
import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';
pluginTester({
plugin: yourPlugin,
tests: {
}
});
Note how pluginTester
does not appear inside any test
/it
block nor
within any hook functions. For advanced use cases, pluginTester
may
appear within one or more describe
blocks, though this is discouraged.
Options
This section lists the options you can pass to babel-plugin-tester. They are all
optional with respect to the following:
plugin
This is used to provide the babel plugin under test. For example:
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../src/identifier-reverse-plugin';
pluginTester({
plugin: identifierReversePlugin,
tests: {
}
});
function identifierReversePlugin() {
return {
name: 'identifier reverse',
visitor: {
Identifier(idPath) {
idPath.node.name = idPath.node.name.split('').reverse().join('');
}
}
};
}
pluginName
This is used as the describe block name and in your tests' names. If
pluginName
can be inferred from the plugin
's name, then it will
be and you do not need to provide this option. If it cannot be inferred for
whatever reason, pluginName
defaults to "unknown plugin"
.
Note that there is a small caveat when relying on pluginName
inference.
pluginOptions
This is used to pass options into your plugin at transform time. If provided,
the object will be lodash.mergeWith
'd with each test
object's pluginOptions
/fixture's pluginOptions
, with the latter
taking precedence. Note that arrays will be concatenated and explicitly
undefined values will unset previously defined values during merging.
preset
This is used to provide the babel preset under test. For example:
import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';
import coolNewBabelPreset from './cool-new-babel-preset.js';
pluginTester({
preset: coolNewBabelPreset,
fixtures: path.join(__dirname, 'fixtures')
});
function identifierReversePlugin() {
return {
name: 'identifier reverse',
visitor: {
Identifier(idPath) {
idPath.node.name = idPath.node.name.split('').reverse().join('');
}
}
};
}
function identifierAppendPlugin() {
return {
name: 'identifier append',
visitor: {
Identifier(idPath) {
idPath.node.name = `${idPath.node.name}_appended`;
}
}
};
}
export function coolNewBabelPreset() {
return { plugins: [identifierReversePlugin, identifierAppendPlugin] };
}
presetName
This is used as the describe block name and in your tests' names.
Defaults to "unknown preset"
.
presetOptions
This is used to pass options into your preset at transform time. If provided,
the object will be lodash.mergeWith
'd with each test
object's presetOptions
/fixture's presetOptions
, with the latter
taking precedence. Note that arrays will be concatenated and explicitly
undefined values will unset previously defined values during merging.
babel
This is used to provide your own implementation of babel. This is particularly
useful if you want to use a different version of babel than what's required by
this package.
babelOptions
This is used to configure babel. If provided, the object will be
lodash.mergeWith
'd with the defaults and each test
object's babelOptions
/fixture's babelOptions
, with the latter
taking precedence. Note that arrays will be concatenated and explicitly
undefined values will unset previously defined values during merging.
Also note that babelOptions.babelrc
and babelOptions.configFile
are set to false
by default, which disables automatic babel configuration
loading. This can be re-enabled if desired.
To simply reuse your project's babel.config.js
or some other
configuration file, set babelOptions
like so:
import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';
pluginTester({
plugin: yourPlugin,
babelOptions: require(path.join('..', 'babel.config.js')),
tests: {
}
});
Custom Plugin and Preset Run Order
By default, when you include a custom list of plugins or presets in
babelOptions
, the plugin or preset under test will always be the final plugin
or preset to run.
For example, consider the myPlugin
plugin:
import { pluginTester } from 'babel-plugin-tester';
pluginTester({
plugin: myPlugin,
pluginName: 'my-plugin',
babelOptions: {
plugins: [
['@babel/plugin-syntax-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
]
}
});
By default, myPlugin
will be invoked after @babel/plugin-syntax-decorators
and @babel/plugin-proposal-class-properties (i.e. myPlugin
is appended by
default).
It is possible to specify a custom ordering using the exported
runPluginUnderTestHere
symbol. For instance, to run myPlugin
after
@babel/plugin-syntax-decorators but before
@babel/plugin-proposal-class-properties:
import { pluginTester, runPluginUnderTestHere } from 'babel-plugin-tester';
pluginTester({
plugin: myPlugin,
pluginName: 'my-plugin',
babelOptions: {
plugins: [
['@babel/plugin-syntax-decorators', { legacy: true }],
runPluginUnderTestHere,
['@babel/plugin-proposal-class-properties', { loose: true }]
]
}
});
Or to run myPlugin
before both @babel/plugin-syntax-decorators and
@babel/plugin-proposal-class-properties:
import { pluginTester, runPluginUnderTestHere } from 'babel-plugin-tester';
pluginTester({
plugin: myPlugin,
pluginName: 'my-plugin',
babelOptions: {
plugins: [
runPluginUnderTestHere,
['@babel/plugin-syntax-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
]
}
});
The same can be done when testing presets. Note that myPreset
is normally
prepended by default since, unlike plugins, presets are run in reverse
order:
import { pluginTester, runPresetUnderTestHere } from 'babel-plugin-tester';
pluginTester({
preset: myPreset,
presetName: 'my-preset',
babelOptions: {
presets: [
'@babel/preset-typescript',
['@babel/preset-react', { pragma: 'dom' }],
runPresetUnderTestHere
]
}
});
In this example, myPreset
will run first instead of last.
title
This is used to specify a custom title for the two top-level describe
blocks, the first enclosing all tests (i.e. describe(title, ...)
)
and the second enclosing all fixtures (i.e.
describe(`${title} fixtures`, ...)
).
Explicitly setting this option will override any defaults or inferred values.
Set to false
to prevent the creation of these enclosing describe blocks.
Otherwise, the title defaults to using pluginName
/presetName
.
filepath
This is used to resolve relative paths provided by the fixtures
option;
the test object properties codeFixture
, outputFixture
, and
execFixture
; and during configuration resolution for prettier.
That is: if the aforesaid properties are not absolute paths, they will be
path.join
'd with the directory name of filepath
.
filepath
is also passed to formatResult
if a more specific path is not
available, and it is used as the default value for babelOptions.filename
in
test objects.
This option defaults to the absolute path of the file that invoked the
pluginTester
function.
For backwards compatibility reasons, filepath
is synonymous with filename
.
They can be used interchangeably, though care must be taken not to confuse the
babel-plugin-tester option filename
with babelOptions.filename
. They are
NOT the same!
endOfLine
This is used to control which line endings both the actual output from babel and
the expected output will be converted to. Defaults to "lf"
.
Options | Description |
---|
"lf" | Use Unix-style line endings |
"crlf" | Use Windows-style line endings |
"auto" | Use the system default line endings |
"preserve" | Use the line endings from the input |
false | Disable line ending conversion entirely |
When disabling line ending conversion, note that Babel will always output
LF even if the input is CRLF.
setup
This function will be run before every test runs, including fixtures. It can
return a function which will be treated as a teardown
function. It can
also return a promise. If that promise resolves to a function, that will be
treated as a teardown
function.
See here for the complete run order.
teardown
This function will be run after every test runs, including fixtures. You can
define this via teardown
or you can return it from the setup
function.
This can likewise return a promise if it is asynchronous.
This function, if provided, will be run after any teardown function returned
by setup
. See here for the complete run order.
formatResult
This function is used to format all babel outputs, and defaults to a function
that invokes prettier. If a prettier configuration file is found,
then that will be used. Otherwise, prettier will use its own default
configuration.
You can also override or entirely disable formatting.
snapshot
Equivalent to snapshot
but applied globally across all test
objects.
fixtureOutputName
Equivalent to fixtureOutputName
but applied globally across all
fixtures.
fixtureOutputExt
Equivalent to fixtureOutputExt
but applied globally across all
fixtures.
titleNumbering
Determines which test titles are prefixed with a number when registering test
blocks (e.g. `1. ${title}`
, `2. ${title}`
, etc). Defaults to
"all"
.
Options | Description |
---|
"all" | All test object and fixtures tests will be numbered |
"tests-only" | Only test object tests will be numbered |
"fixtures-only" | Only fixtures tests will be numbered |
false | Disable automatic numbering in titles entirely |
restartTitleNumbering
Normally, multiple invocations of babel-plugin-tester in the same test
file will share the same test title numbering. For example:
import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';
pluginTester({
plugin: yourPlugin,
tests: { 'test one': testOne, 'test two': testTwo }
});
pluginTester({
plugin: yourPlugin,
tests: { 'test one': testOne, 'test x': testTwo }
});
pluginTester({
plugin: yourPlugin,
tests: { 'test five': testOne }
});
Will result in test blocks with names like:
1. Test one
2. Test two
3. Test one
4. Test x
5. Test five
However, setting this option to true
will restart the numbering:
import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';
pluginTester({
plugin: yourPlugin,
tests: { 'test one': testOne, 'test two': testTwo }
});
pluginTester({
plugin: yourPlugin,
restartTitleNumbering: true,
tests: { 'test one': testOne, 'test x': testTwo }
});
pluginTester({
plugin: yourPlugin,
tests: { 'test five': testOne }
});
Which will result in test blocks with names like:
1. Test one
2. Test two
1. Test one
2. Test x
3. Test five
This option is false
by default.
fixtures
There are two ways to create tests: using the tests
option to provide
one or more test objects or using the fixtures
option described here.
Both can be used simultaneously.
The fixtures
option must be a path to a directory with a structure similar to
the following:
fixtures
├── first-test # test title will be: "1. first test"
│ ├── code.js # required
│ └── output.js # required (unless using the `throws` option)
├── second-test # test title will be: "2. second test"
│ ├── .babelrc.js # optional
│ ├── options.json # optional
│ ├── code.ts # required (other file extensions are allowed too)
│ └── output.js # required (unless using the `throws` option)
└── nested
├── options.json # optional
├── third-test # test title will be: "3. nested > third test"
│ ├── code.mjs # required (other file extensions are allowed too)
│ ├── output.js # required (unless using the `throws` option)
│ └── options.js # optional (overrides props in nested/options.json)
└── x-fourth-test # test title will be: "4. nested > x fourth test"
└── exec.js # required (alternative to code/output structure)
Assuming the fixtures
directory is in the same directory as your test file,
you could use it with the following configuration:
pluginTester({
plugin,
fixtures: path.join(__dirname, 'fixtures')
});
If fixtures
is not an absolute path, it will be path.join
'd with the
directory name of filepath
.
.babelrc
, .babelrc.json
, .babelrc.js
, .babelrc.cjs
, and .babelrc.mjs
config files in fixture directories are technically supported out-of-the-box,
though .mjs
config files will cause a segfault in Jest until this issue
with V8/Chromium is resolved.
And it would run four tests, one for each directory in fixtures
containing a
file starting with "code" or "exec".
code.js
This file's contents will be used as the source code input into babel at
transform time. Any file extension can be used, even a multi-part extension
(e.g. .test.js
in code.test.js
) as long as the file name starts with
code.
; the expected output file will have the same file extension suffix
(i.e. .js
in code.test.js
) as this file unless changed with the
fixtureOutputExt
option.
After being transformed by babel, the resulting output will have whitespace
trimmed, line endings converted, and then get formatted by prettier.
Note that this file cannot appear in the same directory as exec.js
. If
more than one code.*
file exists in a directory, the first one will be used
and the rest will be silently ignored.
output.js
This file, if provided, will have its contents compared with babel's output,
which is code.js
transformed by babel and formatted with prettier.
If this file is missing and neither throws
nor exec.js
are being
used, this file will be automatically generated from babel's output.
Additionally, the name and extension of this file can be changed with the
fixtureOutputName
and fixtureOutputExt
options.
Before being compared to babel's output, this file's contents will have
whitespace trimmed and line endings converted.
Note that this file cannot appear in the same directory as exec.js
.
exec.js
This file's contents will be used as the input into babel at transform time just
like the code.js
file, except the output will be evaluated in the
same CJS context as the test runner itself, meaning it supports features
like a/sync IIFEs, debugging breakpoints (!), and has access to mocked modules,
expect
, require
, __dirname
and __filename
(derived from this file's
path), and other globals/features provided by your test framework. However, the
context does not support import
, top-level await, or any other ESM syntax.
Hence, while any file extension can be used (e.g. .ts
, .vue
, .jsx
), this
file will always be evaluated as CJS.
The test will always pass unless an exception is thrown (e.g. when an expect()
fails).
Use this to make advanced assertions on the output. For example, to test that
babel-plugin-proposal-throw-expressions actually throws, your exec.js
file might contain:
expect(() => throw new Error('throw expression')).toThrow('throw expression');
Keep in mind that, despite sharing a global context, execution will occur in a
separate realm, which means native/intrinsic types will be different.
This can lead to unexpectedly failing tests. For example:
expect(require(`${__dirname}/imported-file.json`)).toStrictEqual({
data: 'imported'
});
This may fail in some test frameworks with the message "serializes to the same
string". This is because the former object's Object
prototype comes from a
different realm than the second object's Object
prototype, meaning the two
objects are not technically strictly equal. However, something like the
following, which creates two objects in the same realm, will pass:
expect(
Object.fromEntries(
Object.entries(require(`${__dirname}/imported-file.json`))
)
).toStrictEqual({ data: 'imported' });
Or even:
expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
JSON.stringify({ data: 'imported' })
);
After being transformed by babel but before being evaluated, the babel output
will have whitespace trimmed, line endings converted, and then get
formatted by prettier.
Note that this file cannot appear in the same directory as code.js
or
output.js
. If more than one exec.*
file exists in a directory, the
first one will be used and the rest will be silently ignored.
options.json
(Or options.js
)
For each fixture, the contents of the entirely optional options.json
file are
lodash.mergeWith
'd with the options provided to
babel-plugin-tester, with the former taking precedence. Note that arrays will be
concatenated and explicitly undefined values will unset previously defined
values during merging.
For added flexibility, options.json
can be specified as options.js
instead
so long as a JSON object is exported via module.exports
. If both files
exist in the same directory, options.js
will take precedence and
options.json
will be ignored entirely.
Fixtures support deeply nested directory structures as well as shared or "root"
options.json
files. For example, placing an options.json
file in the
fixtures/nested
directory would make its contents the "global configuration"
for all fixtures under fixtures/nested
. That is: each fixture would
lodash.mergeWith
the options provided to
babel-plugin-tester, fixtures/nested/options.json
, and the contents of their
local options.json
file as described above.
What follows are the properties you may use if you provide an options file, all
of which are optional:
babelOptions
This is used to configure babel. Properties specified here override
(lodash.mergeWith
) those from the babelOptions
option provided to babel-plugin-tester. Note that arrays will be concatenated
and explicitly undefined values will unset previously defined values during
merging.
pluginOptions
This is used to pass options into your plugin at transform time. Properties
specified here override (lodash.mergeWith
) those from the
pluginOptions
option provided to babel-plugin-tester. Note that arrays
will be concatenated and explicitly undefined values will unset previously
defined values during merging.
Unlike with babel-plugin-tester's options, you can safely mix plugin-specific
properties (like pluginOptions
) with preset-specific properties (like
presetOptions
) in your options files.
presetOptions
This is used to pass options into your preset at transform time. Properties
specified here override (lodash.mergeWith
) those from the
presetOptions
option provided to babel-plugin-tester. Note that arrays
will be concatenated and explicitly undefined values will unset previously
defined values during merging.
Unlike with babel-plugin-tester's options, you can safely mix plugin-specific
properties (like pluginOptions
) with preset-specific properties (like
presetOptions
) in your options files.
title
If provided, this will be used as the title of the test. Otherwise, the
directory name will be used as the title by default (with spaces replacing
dashes).
only
Use this to run only the specified fixture. Useful while developing to help
focus on a small number of fixtures. Can be used in multiple options.json
files.
Requires Jest, an equivalent interface (like Vitest), or a
manually-defined it
object exposing an appropriate only
method.
skip
Use this to skip running the specified fixture. Useful for when you are working
on a feature that is not yet supported. Can be used in multiple options.json
files.
Requires Jest, an equivalent interface (like Vitest), or a
manually-defined it
object exposing an appropriate skip
method.
throws
When using certain values, this property must be used in options.js
instead
of options.json
.
Use this to assert that a particular code.js
file should cause babel to throw
an error during transformation. For example:
{
throws: true,
throws: 'should have this exact message',
throws: /should pass this regex/,
throws: SyntaxError,
throws: err => {
if (err instanceof SyntaxError && /message/.test(err.message)) {
return true;
}
},
}
Be careful using instanceof
across realms as it can lead to strange
behavior with frontend frames/windows and with tools that rely on
Node's VM module (like Jest). Prefer name checks and utilities
like isNativeError
, Array.isArray
, and overriding
Symbol.hasInstance
instead.
If the value of throws
is a class, that class must be a subtype of
Error
or the behavior of babel-plugin-tester is undefined.
For backwards compatibility reasons, throws
is synonymous with error
. They
can be used interchangeably, with throws
taking precedence.
Note that this property cannot be present when using an exec.js
or
output.js
file.
setup
As it requires a function value, this property must be used in options.js
instead of options.json
.
This function will be run before a particular fixture's tests are run. It can
return a function which will be treated as a teardown
function. It can
also return a promise. If that promise resolves to a function, that will be
treated as a teardown
function.
This function, if provided, will run after any setup
function provided
as a babel-plugin-tester option. See here for the complete run order.
teardown
As it requires a function value, this property must be used in options.js
instead of options.json
.
This function will be run after a fixture's tests finish running. You can define
this via teardown
or you can return it from the setup
function. This
can likewise return a promise if it is asynchronous.
This function, if provided, will be run after any teardown function returned
by the setup
property, both of which will run before any
teardown
function provided as a babel-plugin-tester option. See
here for the complete run order.
formatResult
As it requires a function value, this property must be used in options.js
instead of options.json
.
This function is used to format all babel outputs, and defaults to a function
that invokes prettier. If a prettier configuration file is found,
then that will be used. Otherwise, prettier will use its own default
configuration.
You can also entirely disable formatting.
This will override the formatResult
function provided to
babel-plugin-tester.
fixtureOutputName
Use this to provide your own fixture output file name. Defaults to "output"
.
fixtureOutputExt
Use this to provide your own fixture output file extension. Including the
leading period is optional; that is: if you want output.jsx
,
fixtureOutputExt
can be set to either "jsx"
or ".jsx"
. If omitted, the
input fixture's file extension will be used instead.
This is particularly useful if you are testing TypeScript input.
tests
There are two ways to create tests: using the fixtures
option that
leverages the filesystem or using the tests
option described here. Both can be
used simultaneously.
Using the tests
option, you can provide test objects describing your
expected transformations. You can provide tests
as an object of test objects
or an array of test objects. If you provide an object, the object's keys will be
used as the default title of each test. If you provide an array, each test's
default title will be derived from its index and
pluginName
/presetName
.
See the example for more details.
Test Objects
A minimal test object can be:
- A
string
representing code. - An
object
with a code
property.
What follows are the properties you may use if you provide an object, most of
which are optional:
babelOptions
This is used to configure babel. Properties specified here override
(lodash.mergeWith
) those from the babelOptions
option provided to babel-plugin-tester. Note that arrays will be concatenated
and explicitly undefined values will unset previously defined values during
merging.
pluginOptions
This is used to pass options into your plugin at transform time. Properties
specified here override (lodash.mergeWith
) those from the
pluginOptions
option provided to babel-plugin-tester. Note that arrays
will be concatenated and explicitly undefined values will unset previously
defined values during merging.
Unlike with babel-plugin-tester's options, you can safely mix plugin-specific
properties (like pluginOptions
) with preset-specific properties (like
presetOptions
) in your test objects.
presetOptions
This is used to pass options into your preset at transform time. Properties
specified here override (lodash.mergeWith
) those from the
presetOptions
option provided to babel-plugin-tester. Note that arrays
will be concatenated and explicitly undefined values will unset previously
defined values during merging.
Unlike with babel-plugin-tester's options, you can safely mix plugin-specific
properties (like pluginOptions
) with preset-specific properties (like
presetOptions
) in your test objects.
title
If provided, this will be used as the title of the test. Otherwise, the title
will be determined from test object by default.
only
Use this to run only the specified test. Useful while developing to help focus
on a small number of tests. Can be used on multiple tests.
Requires Jest, an equivalent interface (like Vitest), or a
manually-defined it
object exposing an appropriate only
method.
skip
Use this to skip running the specified test. Useful for when you are working on
a feature that is not yet supported. Can be used on multiple tests.
Requires Jest, an equivalent interface (like Vitest), or a
manually-defined it
object exposing an appropriate skip
method.
throws
Use this to assert that a particular test object should cause babel to throw an
error during transformation. For example:
{
throws: true,
throws: 'should have this exact message',
throws: /should pass this regex/,
throws: SyntaxError,
throws: err => {
if (err instanceof SyntaxError && /message/.test(err.message)) {
return true;
}
},
}
Be careful using instanceof
across realms as it can lead to strange
behavior with frontend frames/windows and with tools that rely on
Node's VM module (like Jest). Prefer name checks and utilities
like isNativeError
, Array.isArray
, and overriding
Symbol.hasInstance
instead.
If the value of throws
is a class, that class must be a subtype of
Error
or the behavior of babel-plugin-tester is undefined.
For backwards compatibility reasons, throws
is synonymous with error
. They
can be used interchangeably, with throws
taking precedence.
Note that this property cannot be present when using the output
,
outputFixture
, exec
, execFixture
, or snapshot
properties.
setup
This function will be run before a particular test is run. It can return a
function which will be treated as a teardown
function. It can also
return a promise. If that promise resolves to a function, that will be treated
as a teardown
function.
This function, if provided, will run after any setup
function provided
as a babel-plugin-tester option. See here for the complete run order.
teardown
This function will be run after a test finishes running. You can define this via
teardown
or you can return it from the setup
function. This can
likewise return a promise if it is asynchronous.
This function, if provided, will be run after any teardown function returned
by the setup
property, both of which will run before any
teardown
function provided as a babel-plugin-tester option. See
here for the complete run order.
formatResult
This function is used to format all babel outputs, and defaults to a function
that invokes prettier. If a prettier configuration file is found,
then that will be used. Otherwise, prettier will use its own default
configuration.
You can also entirely disable formatting.
This will override the formatResult
function provided to
babel-plugin-tester.
snapshot
If you would prefer to take a snapshot of babel's output rather than compare it
to something you provide manually, specify snapshot: true
. This will cause
babel-plugin-tester to generate a snapshot containing both the source code
and babel's output.
Defaults to false
.
Note that this property cannot appear in the same test object as the
output
, outputFixture
, exec
, execFixture
, or
throws
properties.
Requires Jest, an appropriate shim or equivalent interface (like
Vitest), or a manually-defined expect
object exposing an appropriate
toMatchSnapshot
method.
code
The code that you want babel to transform using your plugin or preset. This must
be provided unless you are using the codeFixture
or exec
properties instead. If you do not provide the output
or
outputFixture
properties and snapshot
is not truthy, then the
assertion is that this code is unchanged by the transformation.
Before being transformed by babel, any indentation will be stripped as a
convenience for template literals. After being transformed, the resulting output
will have whitespace trimmed, line endings converted, and then get
formatted by prettier.
Note that this property cannot appear in the same test object as the
codeFixture
, exec
, or execFixture
properties.
output
The value of this property will be compared with babel's output.
Before being compared to babel's output, this value will have whitespace
trimmed, line endings converted, and any indentation stripped as a
convenience for template literals.
Note that this property cannot appear in the same test object as the
outputFixture
, exec
, execFixture
, throws
, or
snapshot
properties.
exec
The provided source will be transformed just like the code
property,
except the output will be evaluated in the same CJS context as the
test runner itself, meaning it supports features like a/sync IIFEs, debugging
breakpoints (!), and has access to mocked modules, expect
, require
,
__dirname
and __filename
(derived from available path info and falling back
on filepath
), and other globals/features provided by your test
framework. However, the context does not support import
, top-level await, or
any other ESM syntax. Hence, while any file extension can be used (e.g. .ts
,
.vue
, .jsx
), this file will always be evaluated as CJS.
The test will always pass unless an exception is thrown (e.g. when an expect()
fails).
Use this to make advanced assertions on the output. For example, you can test
that babel-plugin-proposal-throw-expressions actually throws using the
following:
{
exec: `
expect(() => throw new Error('throw expression')).toThrow('throw expression');
`;
}
Keep in mind that, despite sharing a global context, execution will occur in a
separate realm, which means native/intrinsic types will be different.
This can lead to unexpectedly failing tests. For example:
expect(require(`${__dirname}/imported-file.json`)).toStrictEqual({
data: 'imported'
});
This may fail in some test frameworks with the message "serializes to the same
string". This is because the former object's Object
prototype comes from a
different realm than the second object's Object
prototype, meaning the two
objects are not technically strictly equal. However, something like the
following, which creates two objects in the same realm, will pass:
expect(
Object.fromEntries(
Object.entries(require(`${__dirname}/imported-file.json`))
)
).toStrictEqual({ data: 'imported' });
Or even:
expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
JSON.stringify({ data: 'imported' })
);
After being transformed by babel but before being evaluated, the babel output
will have whitespace trimmed, line endings converted, and then get
formatted by prettier.
Note that this property cannot appear in the same test object as the
execFixture
, code
, codeFixture
, output
,
outputFixture
, throws
, or snapshot
properties.
codeFixture
If you would rather put your code
in a separate file, you can specify a
file path here instead. If it is an absolute path, then that's the file that
will be loaded. Otherwise, codeFixture
will be path.join
'd with the
directory name of filepath
.
After being transformed by babel, the resulting output will have whitespace
trimmed, line endings converted, and then get formatted by prettier.
Like code
, this property cannot appear in the same test object as the
exec
or execFixture
properties, nor the code
property
(obviously).
If you find you are using this property more than a couple of times, consider
using fixtures
instead.
For backwards compatibility reasons, codeFixture
is synonymous with
fixture
. They can be used interchangeably, though care must be taken not to
confuse the test object property fixture
with the babel-plugin-tester option
fixtures
, the latter being plural.
outputFixture
If you would rather put your output
in a separate file, you can specify
a file path here instead. If it is an absolute path, then that's the file that
will be loaded. Otherwise, outputFixture
will be path.join
'd with the
directory name of filepath
.
Before being compared to babel's output, this file's contents will have
whitespace trimmed and line endings converted.
Like output
, this property cannot appear in the same test object as the
exec
, execFixture
, throws
, or snapshot
properties, nor the output
property (obviously).
If you find you are using this property more than a couple of times, consider
using fixtures
instead.
execFixture
If you would rather put your exec
in a separate file, you can specify a
file path here instead. If it is an absolute path, then that's the file that
will be loaded. Otherwise, execFixture
will be path.join
'd with the
directory name of filepath
.
After being transformed by babel but before being evaluated, the babel output
will have whitespace trimmed, line endings converted, and then get
formatted by prettier.
Like exec
, this property cannot appear in the same test object as the
code
, codeFixture
, output
, outputFixture
,
throws
, or snapshot
properties, nor the exec
property
(obviously).
If you find you are using this property more than a couple of times, consider
using fixtures
instead.
Examples
Simple Example
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';
pluginTester({
plugin: identifierReversePlugin,
snapshot: true,
tests: [
{
code: "'hello';"
},
{
snapshot: false,
code: 'var hello = "hi";',
output: 'var olleh = "hi";'
},
`
function sayHi(person) {
return 'Hello ' + person + '!'
}
console.log(sayHi('Jenny'))
`
]
});
Full Example
import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';
pluginTester({
plugin: identifierReversePlugin,
pluginName: 'identifier reverse',
pluginOptions: {
optionA: true
},
title: 'describe block title',
filepath: __filename,
babelOptions: {
parserOpts: {},
generatorOpts: {},
babelrc: false,
configFile: false
},
snapshot: false,
formatResult: customFormatFunction,
tests: {
'does not change code with no identifiers': '"hello";',
'changes this code': {
code: 'var hello = "hi";',
output: 'var olleh = "hi";'
}
},
tests: [
'"hello";',
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";'
},
{
title: 'unchanged code',
codeFixture: path.join(
__dirname,
'..',
'fixtures',
'codeFixture-unchanging.js'
)
},
{
codeFixture: path.join('..', 'fixtures', 'codeFixture.js'),
outputFixture: path.join('..', 'fixtures', 'outputFixture.js')
},
{
code: `
function sayHi(person) {
return 'Hello ' + person + '!';
}
`,
snapshot: true
},
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
pluginOptions: {
optionA: false
}
},
{
title: 'unchanged code',
code: '"no change";',
setup() {
return function teardown() {
};
},
teardown() {
}
},
{
exec: `
const hello = "hi";
// The plugin will reverse ALL identifiers, even globals like "expect"!
tcepxe(hello)['toBe']("hi");
`
}
]
});
Fixtures Examples
See fixtures
for an example directory layout or check out the use of
babel-plugin-tester fixtures in some of these other projects:
Appendix
Testing Framework Compatibility
This package was originally tested on and built to work with Jest, but it
is also tested against Vitest, Mocha, Jasmine, and
node:test
. See below for details.
Jest
All babel-plugin-tester features work with Jest. No further action is necessary
🚀
Vitest
All babel-plugin-tester features work with Vitest, though Vitest don't provide
global APIs by default. You can either supply some interoperability code (see
Jasmine or node:test
below for an example) or run Vitest with the --globals
CLI option.
Mocha
Most babel-plugin-tester features work with Mocha, except Mocha does not
natively support snapshots.
Jasmine
Most babel-plugin-tester features work with Jasmine if you define the
appropriate globals:
import { pluginTester } from 'babel-plugin-tester';
globalThis.it.skip = globalThis.xit;
globalThis.it.only = globalThis.fit;
pluginTester(...);
However, Jasmine does not natively support snapshots.
node:test
Most babel-plugin-tester features work with node:test
if you define the
appropriate globals:
import { describe, it } from 'node:test';
import { pluginTester } from 'babel-plugin-tester';
globalThis.describe = describe;
globalThis.it = it;
globalThis.it.only = (...args) => it(args[0], { only: true }, args[1]);
pluginTester(...);
However, node:test
does not natively support snapshots.
Other Frameworks
Other testing frameworks and test runners should also work so long as they
define standard describe
and it
globals with async support, or appropriate
interoperability code is used like in the above Jasmine and node:test
examples.
Using Babel for Configuration Loading
babelOptions.babelrc
and babelOptions.configFile
are set to
false
by default. This way, you can manually import (or provide an object
literal) the exact configuration you want to apply rather than relying on
babel's somewhat complex configuration loading rules. However, if your
plugin, preset, or project relies on a complicated external setup to do its
work, and you do not mind the default run order, you can leverage
babel's automatic configuration loading via the babelOptions.babelrc
and/or babelOptions.configFile
options.
Fixtures provided via the fixtures
option do not need to provide a
separate babelOptions.filename
since it will be set automatically. This
section only applies to test objects.
When relying on babelOptions.babelrc
, you must also provide a
babelOptions.filename
for each test object that does not include a
codeFixture
or execFixture
property. For example:
pluginTester({
plugin,
tests: [
{
code: '"blah"',
babelOptions: {
babelrc: true,
filename: path.join(__dirname, 'some-file.js')
}
},
{
code: '"hi"',
babelOptions: {
babelrc: true,
filename: path.join(__dirname, 'some-other-file.js')
}
},
{
codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
},
{
execFixture: path.join(__dirname, 'fixtures', 'my-script.js')
}
]
});
This file does not actually have to exist either, so you can use whatever value
you want for filename
as long as the .babelrc
file is resolved
properly. Hence, the above example could be simplified further:
pluginTester({
plugin,
babelOptions: {
babelrc: true,
filename: __filename
},
tests: [
'"blah"',
'"hi"',
{
codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
},
{
execFixture: path.join(__dirname, 'fixtures', 'my-script.js')
}
]
});
pluginName
Inference Caveat
Inferring pluginName
during testing requires invoking the plugin
at least twice: once outside of babel to check for the plugin's name and then
again when run by babel. This is irrelevant to babel-plugin-tester (even if your
plugin crashes when run outside of babel) and to the overwhelming majority of
babel plugins in existence. This only becomes a problem if your plugin is
aggressively stateful, which is against the babel handbook on plugin
design.
For example, the following plugin which replaces an import specifier using a
regular expression will exhibit strange behavior due to being invoked twice:
let source;
function badNotGoodPlugin({ assertVersion, types: t }) {
assertVersion(7);
if (!source) {
source = (value, original, replacement) => {
return t.stringLiteral(value.replace(original, replacement));
};
}
return {
name: 'bad-bad-not-good',
visitor: {
ImportDeclaration(path, state) {
path.node.source = source(
path.node.source.value,
state.opts.originalRegExp,
state.opts.replacementString
);
}
}
};
}
pluginTester({
plugin: badNotGoodPlugin,
pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});
If you still want to use global state despite the handbook's advice, either
initialize global state within your visitor:
let source;
function okayPlugin({ assertVersion, types: t }) {
assertVersion(7);
return {
name: 'okay',
visitor: {
Program: {
enter() {
if (!source) {
source = (value, original, replacement) => {
return t.stringLiteral(value.replace(original, replacement));
};
}
}
},
ImportDeclaration(path, state) {
path.node.source = source(
path.node.source.value,
state.opts.originalRegExp,
state.opts.replacementString
);
}
}
};
}
pluginTester({
plugin: okayPlugin,
pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});
Or do things the proper way and just use local state instead:
function betterPlugin({ assertVersion, types: t }) {
assertVersion(7);
const source = (value, original, replacement) => {
return t.stringLiteral(value.replace(original, replacement));
};
return {
name: 'better',
visitor: {
ImportDeclaration(path, state) {
path.node.source = source(
path.node.source.value,
state.opts.originalRegExp,
state.opts.replacementString
);
}
}
};
}
pluginTester({
plugin: betterPlugin,
pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});
Custom Snapshot Serialization
If you are using Jest and snapshots, then the snapshot output could have a bunch
of bothersome \"
to escape quotes. This is because, when Jest serializes a
string, it will wrap everything in double quotes. This is not a huge deal, but
it makes the snapshots harder to read, so we automatically add a snapshot
serializer for you to remove those. Note that this serializer is added globally
and thus will affect all snapshots taken in the test file, even those outside
of babel-plugin-tester.
If you would like to disable this feature, then use the "pure" import (also
disables formatting of babel output with prettier):
- import { pluginTester } from 'babel-plugin-tester'
+ import { pluginTester } from 'babel-plugin-tester/pure'
It seems recent versions of Jest already ship with easier-to-read snapshots,
making this serializer redundant. Therefore, the built-in custom serializer
will likely be removed entirely in a future version of babel-plugin-tester.
Formatting Output with Prettier
By default, a formatter is used which formats all babel output with
prettier. It will look for a prettier configuration file relative to
the file that's being tested or the current working directory. If
it cannot find one, then it uses the default configuration for prettier.
This makes your snapshots easier to read and your expectations easier to write,
but if you would like to disable this feature, you can either use the pure
import to disable automatic formatting (along with snapshot serialization)
or you can override the formatResult
option manually like so:
pluginTester({
formatResult: (r) => r
});
Built-In Debugging Support
This package uses debug under the hood. To view all possible debugging
output, including the results of all babel transformations, set the
DEBUG='babel-plugin-tester,babel-plugin-tester:*'
environment variable
when running your tests.
For example:
NODE_ENV='test' DEBUG='babel-plugin-tester,babel-plugin-tester:*' DEBUG_DEPTH='1' npx jest
Available Debug Namespaces
The following debug namespaces are available for activation:
babel-plugin-tester:index
babel-plugin-tester:formatter
babel-plugin-tester:serializer
babel-plugin-tester:tester
babel-plugin-tester:tester:resolve-base
babel-plugin-tester:tester:resolve-env
babel-plugin-tester:tester:normalize
babel-plugin-tester:tester:normalize:create-desc
babel-plugin-tester:tester:normalize:create-fix
babel-plugin-tester:tester:normalize:create-obj
babel-plugin-tester:tester:register
babel-plugin-tester:tester:wrapper
babel-plugin-tester:tester:test
babel-plugin-tester:tester:validate
babel-plugin-tester:tester:read-opts
babel-plugin-tester:tester:read-code
babel-plugin-tester:tester:eol
babel-plugin-tester:tester:finalize
The babel-plugin-tester:tester
namespace and its sub-namespaces each have an
additional verbose
sub-namespace that can be activated or deactivated at will,
e.g. babel-plugin-tester:tester:verbose
and
babel-plugin-tester:tester:wrapper:verbose
.
For example, to view all debug output except verbose output:
NODE_ENV='test' DEBUG='babel-plugin-tester,babel-plugin-tester:*,-*:verbose' npx jest
TEST_ONLY
/TEST_NUM_ONLY
and TEST_SKIP
/TEST_NUM_SKIP
Environment Variables
The optional TEST_ONLY
and TEST_SKIP
environment variables are recognized by
babel-plugin-tester, allowing you to control which tests are run in an adhoc
fashion without having to modify your test configuration code.
The values of these variables will be transformed into regular expressions via
RegExp(value, 'u')
and matched against each test/fixture title (not including
automatically assigned numbers prefixed to titles). Tests with titles that
match TEST_ONLY
will be run while all others are skipped. On the other hand,
tests with titles that match TEST_SKIP
will be skipped while others are run.
For example, to skip the test titled "this is the name of a failing unit test":
TEST_SKIP='name of a failing' npx jest
Given both TEST_ONLY
and TEST_SKIP
, tests matched by TEST_SKIP
will
always be skipped, even if they are also matched by TEST_ONLY
. These
environment variables also override both the fixture-specific
skip
/only
and test object skip
/only
properties if they conflict.
In addition to TEST_ONLY
and TEST_SKIP
, you can also target tests
specifically by their automatically assigned number using TEST_NUM_ONLY
and TEST_NUM_SKIP
. These environment variables function identically to their
counterparts except they accept one or more numbers separated by commas (spaces
around commas are ignored) instead of regular expressions. Inclusive ranges
(e.g. 4-9
) are also supported.
For example, the following will skip tests numbered 1, 3, 5, and 6-10
(inclusive):
TEST_NUM_SKIP='5,1, 6-10,, 3,' npx jest
TEST_NUM_ONLY
and TEST_NUM_SKIP
are meaningless if titleNumbering
is
false
or your tests are otherwise unnumbered, and may match multiple tests if
automatic numbering is restarted.
setup
and teardown
Run Order
For each test object and fixture test, setup and teardown functions are run in
the following order:
- Base
setup
. - Test object
setup
/ fixture setup
. - Test object / fixture test is run.
- Any function returned by test object
setup
/ fixture setup
. - Test object
teardown
/ fixture teardown
. - Any function returned by base
setup
. - Base
teardown
.
Inspiration
The API was inspired by:
Issues
Looking to contribute? Look for the Good First Issue
label.
🐛 Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
See Bugs
💡 Feature Requests
Please file an issue to suggest new features. Vote on feature requests by adding
a 👍. This helps maintainers prioritize what to work on.
See Feature Requests
Contributors ✨
Thanks goes to these people (emoji key):
This project follows the all-contributors specification.
Contributions of any kind welcome!
License
MIT