Utilities for testing babel plugins and presets.
You're writing a babel plugin or preset and want to write tests for it too.
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 should work with Mocha, Jasmine, and any other framework
that defines standard it
/describe
/expect
globals.
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
ESM:
import { pluginTester } from 'babel-plugin-tester';
CJS:
const { pluginTester } = require('babel-plugin-tester');
For backwards compatibility reasons, a default export is also available but its use should be avoided.
/* file: test/unit.test.js */
import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';
pluginTester({
plugin: yourPlugin,
tests: {
/* Your test objects */
}
});
Note how
pluginTester
does not appear inside anytest
/it
/describe
block.
This section lists the options you can pass to babel-plugin-tester. They are all optional with respect to the following:
- When testing a preset, the
preset
option is required. - When testing a plugin, the
plugin
option is required. - You must test either a preset or a plugin.
- You cannot use preset-specific options (
preset
,presetName
,presetOptions
) and plugin-specific options (plugin
,pluginName
,pluginOptions
) at the same time.
This is used to provide the babel plugin under test. For example:
/* file: test/unit.test.js */
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../src/identifier-reverse-plugin';
pluginTester({
plugin: identifierReversePlugin,
tests: {
/* Your test objects */
}
});
/* file: src/identifier-reverse-plugin.js */
// Normally you would import this from your plugin module
function identifierReversePlugin() {
return {
name: 'identifier reverse',
visitor: {
Identifier(idPath) {
idPath.node.name = idPath.node.name.split('').reverse().join('');
}
}
};
}
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 don't 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.
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.
This is used to provide the babel preset under test. For example:
/* file: cool-new-babel-preset.test.js */
import { pluginTester } from 'babel-plugin-tester';
import coolNewBabelPreset from 'cool-new-babel-preset.js';
pluginTester({
preset: coolNewBabelPreset,
// A path to a directory containing your test fixtures
fixtures: `${__dirname}/__fixtures__`
});
/* file: cool-new-babel-preset.js */
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] };
}
This is used as the describe block name and in your tests' names.
Defaults to "unknown preset"
.
This is used to pass options into your preset at transform time. This option can be overridden using test object properties or fixture options.
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 included in this package.
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 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 { pluginTester } from 'babel-plugin-tester';
pluginTester({
plugin: yourPlugin,
// ...
babelOptions: require('../babel.config.js'),
// ...
tests: {
/* Your test objects */
}
});
##### Custom Plugin and Preset Run Order
By default, when you include a custom list of [plugins][28] or [presets][2] in
`babelOptions`, the plugin or preset under test will always be the final plugin
or preset to run.
For example, consider the `myPlugin` plugin:
```javascript
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.
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:
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 since, unlike
plugins, presets are run in reverse order.
This is used to specify a custom title for the describe block (overriding everything else).
This is used to resolve relative paths provided by the fixtures
option
and the two test object properties codeFixture
and
outputFixture
. If these are not absolute paths, they will be
path.join
'd with the directory name of filepath
.
filepath
is also passed to formatResult
(fixture option) and
formatResult
(test object property).
Defaults to the absolute path of the file that invoked the pluginTester
function.
For backwards compatibility reasons,
filepath
is synonymous withfilename
. They can be used interchangeably, though care must be taken not to confuse the babel-plugin-tester optionfilename
withbabelOptions.filename
. They are NOT the same!
This is used to control which line endings the output from babel should have.
Options | Description |
---|---|
lf |
Unix - default |
crlf |
Windows |
auto |
Use the system default |
preserve |
Use the line ending from the input |
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: "first test"
βΒ Β βββ code.js # required
βΒ Β βββ output.js # required (unless using the `throws` option)
βββ second-test # test title will be: "second test"
β βββ .babelrc # optional
β βββ options.json # optional
β βββ code.js # required
β βββ output.js # required (unless using the `throws` option)
βββ nested
βββ options.json # optional
βββ third-test # test title will be: "nested > third test"
β βββ code.js # required
β βββ output.js # required (unless using the `throws` option)
β βββ options.js # optional (overrides props in nested/options.json)
βββ fourth-test # test title will be: "nested > 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 bepath.join
'd with the directory name offilepath
.
And it would run four tests, one for each directory in __fixtures__
.
This file's contents will be used as the input into babel at transform time.
Indentation is not stripped nor are the contents of the file trimmed before being passed to babel for transformation.
This file, if provided, will have its contents compared with babel's output,
which is code.js
transformed by babel and formatted with prettier.
This file must be provided unless the throws
property is present in
options.json
. Additionally, the extension of the output file can be
changed with the fixtureOutputExt
property.
Indentation is not stripped nor are the contents of the file trimmed before being compared to babel's output.
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 context as the the test runner itself, meaning it has access to
expect
, require
, etc. Use this to make advanced assertions on the output.
The test will pass unless an exception is thrown (e.g. when an expect()
fails).
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');
However, note that this file cannot appear in the same directory as
code.js
or output.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. 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 (or exports from options.js
) as described in
the previous paragraph.
What follows are the available properties, all of which are optional:
This is used to configure babel. Properties specified here override
(lodash.mergewith
) those from the babelOptions
option provided to babel-plugin-tester.
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.
If provided, this will be used as the title of the test (overriding the directory name).
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.
Use this to skip running the specified fixture. Useful for when you're working
on a feature that is not yet supported. Can be used in multiple options.json
files.
When using certain values, this option must be used in
options.js
instead ofoptions.json
.
Use this to assert that a particular code.js
file should be throwing an error
during transformation. For example:
{
// ...
throws: true,
throws: 'should have this exact message',
throws: /should pass this regex/,
throws: SyntaxError, // Should be instance of this constructor
throws: err => {
if (err instanceof SyntaxError && /message/.test(err.message)) {
return true; // Test will fail if this function doesn't return `true`
}
},
}
For backwards compatibility reasons,
throws
is synonymous witherror
. They can be used interchangeably.
Note that this property is ignored when using an exec.js
file.
As it requires a function value, this option must be used in
options.js
instead ofoptions.json
.
If you need something set up before a particular fixture's tests are run, you
can do this with setup
. This function will be run before the fixture runs. 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.
As it requires a function value, this option must be used in
options.js
instead ofoptions.json
.
If you set up some state, it's quite possible you want to tear it down. Use this
function to clean up after a fixture's tests finish running. You can either
define this as its own property, or you can return it from the setup
function. This can likewise return a promise if it's asynchronous.
This property takes precedence over anything returned by the setup
property.
As it requires a function value, this option must be used in
options.js
instead ofoptions.json
.
This defaults to a function which formats your code output with prettier. If you have prettier configured, then it will use your configuration. If you don't, then it will use a default configuration.
You can also specify your own custom formatter:
function customFormatter(code, { filepath }) {
// Your custom formatting happens here
return formattedCode;
}
Learn more about the built-in formatter below.
The use case for this originally was for testing transforms and formatting their result with
prettier-eslint
.
Use this to provide your own fixture output file name. Defaults to output
.
Use this to provide your own fixture output file extension. This is particularly useful if you are testing TypeScript input. If omitted, the fixture's input file extension will be used instead.
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.
A minimal test object can be:
Here are the available properties if you provide an object:
This is used to configure babel. Properties specified here override
(lodash.mergewith
) those from the babelOptions
option provided to babel-plugin-tester.
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.
If provided, this will be used as the title of the test (overriding everything else).
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.
Use this to skip running the specified test. Useful for when you're working on a feature that is not yet supported. Can be used on multiple tests.
If a particular test case should be throwing an error, you can test that using one of the following:
{
// ...
throws: true,
throws: 'should have this exact message',
throws: /should pass this regex/,
throws: SyntaxError, // Should be instance of this constructor
throws: err => {
if (err instanceof SyntaxError && /message/.test(err.message)) {
return true; // Test will fail if this function doesn't return `true`
}
},
}
For backwards compatibility reasons,
throws
is synonymous witherror
. They can be used interchangeably.
Note that this property is ignored when using the exec
property.
If you need something set up before a particular test is run, you can do this
with setup
. This function will be run before the test runs. 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.
If you set up some state, it's quite possible you want to tear it down. Use this
function to clean up after a test finishes running. You can either define this
as its own property, or you can return it from the setup
function. This
can likewise return a promise if it's asynchronous.
This property takes precedence over anything returned by the setup
property.
This defaults to a function which formats your code output with prettier. If you have prettier configured, then it will use your configuration. If you don't, then it will use a default configuration.
You can also specify your own custom formatter:
function customFormatter(code, { filepath }) {
// Your custom formatting happens here
return formattedCode;
}
Learn more about the built-in formatter below.
The use case for this originally was for testing transforms and formatting their result with
prettier-eslint
.
If you'd prefer to take a snapshot of your output rather than compare it to
something you hard-code, then specify snapshot: true
. This will take a
snapshot with both the source code and the output, making the snapshot easier to
understand.
The code that you want to run through your plugin or preset. This must be
provided unless you're using the codeFixture
or exec
properties
instead. If you do not provide the output
or outputFixture
properties and snapshot
is not true
, then the assertion is that this
code is unchanged by the transformation.
Indentation is not stripped nor is the value trimmed before being passed to babel.
The value of this property, if provided, will be compared with babel's output,
which is code
transformed by babel and formatted with prettier.
Said value will have any indentation stripped and will be trimmed as a convenience for template literals.
If you'd rather put your code
in a separate file, you can specify a file
name here instead. If it's 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
.
Indentation is not stripped nor are the contents of the file trimmed before being passed to babel for transformation.
If you find you're using this option more than a couple of times, consider using
fixtures
instead.
For backwards compatibility reasons,
codeFixture
is synonymous withfixture
. They can be used interchangeably, though care must be taken not to confuse the test object optionfixture
with the babel-plugin-tester optionfixtures
, the latter being plural.
If you'd rather put your output
in a separate file, you can specify a
file name here instead. If it's 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
.
Indentation is not stripped nor are the contents of the file trimmed before being compared to babel's output.
If you find you're using this option more than a couple of times, consider using
fixtures
instead.
The provided source will be transformed just like the code
property,
except the output will be evaluated in the same context as the the test runner
itself, meaning it has access to expect
, require
, etc. Use this to make
advanced assertions on the output.
The test will pass unless an exception is thrown (e.g. when an expect()
fails).
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');
`;
}
However, this property cannot appear in the same test object as the
code
, output
, codeFixture
, or outputFixture
properties.
The rest of the options you pass to babel-plugin-tester will be
lodash.mergewith
'd with each test object and
fixture options with the latter taking precedence.
Invalid options will trigger a warning. Invalid option combinations will throw an error.
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';
// NOTE: you can use beforeAll, afterAll, beforeEach, and afterEach as usual
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'))
`
]
});
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';
pluginTester({
// One (and ONLY ONE) of the two following lines MUST be included
plugin: identifierReversePlugin,
//preset: coolNewBabelPreset,
// Usually unnecessary if returned with the plugin. This will default to
// 'unknown plugin' if a name cannot otherwise be inferred
pluginName: 'identifier reverse',
// Unlike with pluginName, there is no presetName inference. This will default
// to 'unknown preset' if a name is not provided
//presetName: 'cool-new-babel-preset',
// Used to test specific plugin options
pluginOptions: {
optionA: true
},
//presetOptions: {
// optionB: false,
//}
// Defaults to the plugin name
title: 'describe block title',
// Only useful if you're using fixtures, fixture, or outputFixture options.
// Defaults to the absolute path of the file the pluginTester function was
// invoked in, which is equivalent to the following line:
filepath: __filename,
// These are the defaults that will be lodash.mergeWith'd with the provided
// babelOptions option
babelOptions: {
parserOpts: {},
generatorOpts: {},
babelrc: false,
configFile: false
},
// Use Jest snapshots (only works with Jest)
snapshot: false,
// Defaults to a function that formats with prettier
formatResult: customFormatFunction,
// Tests as an object
tests: {
// The key is the title. The value is the code that is unchanged (because
// snapshot == false). Test title will be: "1. does not change code with no
// identifiers"
'does not change code with no identifiers': '"hello";',
// Test title will be: "2. changes this code"
'changes this code': {
// Input to the plugin
code: 'var hello = "hi";',
// Expected output
output: 'var olleh = "hi";'
}
},
// Tests as an array
tests: [
// Should be unchanged by the plugin (because snapshot == false). Test title
// will be: "1. identifier reverse"
'"hello";',
{
// Test title will be: "2. identifier reverse"
code: 'var hello = "hi";',
output: 'var olleh = "hi";'
},
{
// Test title will be: "3. unchanged code"
title: 'unchanged code',
// Because this is an absolute path, the filepath option above will not
// be used to resolve this path
fixture: path.join(__dirname, 'some-path', 'unchanged.js')
// No output, outputFixture, or snapshot, so the assertion will be that
// the plugin does not change this code
},
{
// Because these are not absolute paths, they will be joined with the
// directory of the filepath option provided above
fixture: '__fixtures__/changed.js',
// Because outputFixture is provided, the assertion will be that the
// plugin will change the contents of "changed.js" to the contents of
// "changed-output.js"
outputFixture: '__fixtures__/changed-output.js'
},
{
// As a convenience, this will have the indentation striped and it will
// be trimmed
code: `
function sayHi(person) {
return 'Hello ' + person + '!';
}
`,
// This will take a Jest snapshot. The snapshot will have both the source
// code and the transformed version to make the snapshot file easier to
// understand
snapshot: true
},
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
// This can be used to overwrite pluginOptions (set above)
pluginOptions: {
optionA: false
}
},
{
title: 'unchanged code',
code: "'no change';",
setup() {
// Runs before this test
return function teardown() {
// Runs after this tests
};
// Can also return a promise
},
teardown() {
// Runs after this test
// Can return a promise
}
},
{
// This source will be transformed just like the code property, except the
// produced code will be evaluated in the same context as the the test
// runner. Use this to make more advanced assertions on the output.
exec: `
const hello = "hi";
// The plugin will reverse ALL identifiers, even globals like "expect"!
tcepxe(hello)['toBe']("hi");
`
}
]
});
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 don't mind the default run order, you can leverage babel's
automatic configuration loading via the babelOptions.babelrc
and/or
babelOptions.configFile
options.
When relying on babelOptions.babelrc
, you must also provide a
babelOptions.filename
for each test object that doesn't include a
codeFixture
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')
}
},
{
fixture: path.join(__dirname, '__fixtures__/my-file.js')
}
]
});
Fixtures provided via the
fixtures
option do not need to provide afilename
.
This file doesn't 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,
// This configuration applies to *all* tests!
babelOptions: {
babelrc: true,
filename: __filename
},
tests: [
'"blah"',
'"hi"',
{
fixture: path.join(__dirname, '__fixtures__/my-file.js')
}
]
});
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:
/* -*-*- BAD CODE DO NOT USE -*-*- */
let source;
// vvv When first invoked outside of babel, all passed arguments are mocks vvv
function badNotGoodPlugin({ assertVersion, types: t }) {
// ^^^ Which means assertVersion is mocked and t is undefined ^^^
assertVersion(7);
// vvv So don't memoize `t` here (which among other things is poor design) vvv
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";' }]
});
// Result: error!
// TypeError: Cannot read properties of undefined (reading 'stringLiteral')
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() {
// vvv Initialize global state in a safe place vvv
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";' }]
});
// Result: works!
Or do things the proper way and just use local state instead:
function betterPlugin({ assertVersion, types: t }) {
assertVersion(7);
// vvv Use local state instead so t is memoized properly vvv
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";' }]
});
// Result: works!
If you're 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 isn't 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, even those outside of
babel-plugin-tester.
If you'd 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'
By default, a formatter is included which formats all babel output with prettier. It will look for a prettier configuration relative to the file that's being tested or the current working directory. If it can't 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'd 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:
pluginTester({
// ...
formatResult: (r) => r
// ...
});
This package uses debug under the hood; more verbose output, including the
results of all babel transformations, can be activated by passing the
DEBUG=babel-plugin-tester,babel-plugin-tester:*
environment variable when
running babel-plugin-tester.
babel-plugin-tester:index
babel-plugin-tester:tester
babel-plugin-tester:formatter
babel-plugin-tester:serializer
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 modifying 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. 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.
Given both TEST_ONLY
and TEST_SKIP
, tests matched by TEST_SKIP
will
always be skipped, even if they're also matched by TEST_ONLY
.
The API was inspired by:
- ESLint's RuleTester.
- @thejameskyle's tweet.
- Babel's own
@babel/helper-plugin-test-runner
.
Looking to contribute? Look for the Good First Issue label.
Please file an issue for bugs, missing documentation, or unexpected behavior.
Please file an issue to suggest new features. Vote on feature requests by adding a π. This helps maintainers prioritize what to work on.
Thanks goes to these people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT