Package detail

babel-plugin-tester

babel-utils419.2kMIT12.0.0

Utilities for testing babel plugins

babel, plugin, tester

readme

Utilities for testing babel plugins 🧪


[![Black Lives Matter!][x-badge-blm-image]][x-badge-blm-link] [![Last commit timestamp][x-badge-lastcommit-image]][x-badge-repo-link] [![Codecov][x-badge-codecov-image]][x-badge-codecov-link] [![Source license][x-badge-license-image]][x-badge-license-link] [![Uses Semantic Release!][x-badge-semanticrelease-image]][x-badge-semanticrelease-link] [![NPM version][x-badge-npm-image]][x-badge-npm-link] [![Monthly Downloads][x-badge-downloads-image]][x-badge-npm-link]


babel-plugin-tester

This is a fairly simple abstraction to help you write tests for your babel plugin or preset. It was built to work with [Jest][4], but most of the functionality will work with [Mocha][5], [Jasmine][6], [node:test][7], [Vitest][8], and any other test runner that defines standard describe and it globals with async support (see [appendix][9]).

This package is tested on both Windows and nix (Ubuntu) environments.



Install

npm install --save-dev babel-plugin-tester

Usage

To use babel-plugin-tester:

  1. Import babel-plugin-tester into your test file.
  2. Invoke pluginTester in your test file.
  3. Execute your test file.

Import

ESM:

import { pluginTester } from 'babel-plugin-tester';

CJS:

const { pluginTester } = require('babel-plugin-tester');

Invoke

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from 'universe:your-plugin';

pluginTester({
  plugin: yourPlugin,
  tests: {
    /* Your test objects */
  }
});

[!TIP]

Note how pluginTester does not appear inside any test/it block nor within any [hook functions][10]. For advanced use cases, pluginTester may appear within one or more describe blocks, though this is discouraged.

Execute

In your terminal of choice:

# Prettier@3 requires --experimental-vm-modules for older Node versions
NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest

Configure

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][11] option is required.
  • When testing a plugin, the [plugin][12] option is required.
  • You must test either a preset or a plugin.
  • You cannot use preset-specific options ([preset][11], [presetName][13], [presetOptions][14]) and plugin-specific options ([plugin][12], [pluginName][15], [pluginOptions][16]) at the same time.

plugin

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 'universe: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('');
      }
    }
  };
}

pluginName

This is used as the [describe block name][17] and in your [tests' names][18]. If pluginName can be inferred from the [plugin][12]'s [name][19], 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][20] 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][lodash.mergewith]'d with each [test object's pluginOptions][21]/[fixture's pluginOptions][22], 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:

/* file: cool-new-babel-preset.test.js */

import path from 'node:path';
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: path.join(__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] };
}

presetName

This is used as the [describe block name][17] and in your [tests' names][18]. 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][lodash.mergewith]'d with each [test object's presetOptions][23]/[fixture's presetOptions][24], 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][lodash.mergewith]'d with the [defaults][25] and each [test object's babelOptions][26]/[fixture's babelOptions][27], with the latter taking precedence.

Be aware that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

[!IMPORTANT]

For babel-plugin-tester@>=12, [duplicate entries][2] in [babelOptions.plugins][55] and [babelOptions.presets][73] are reduced, with latter entries completely overwriting any that came before. In other words: the last duplicate plugin or preset configuration wins. They are not merged. This makes it easy to provide an alternative one-off configuration for a plugin or preset that is also used elsewhere, such as a project's root babel.config.js file.

Attempting the same with babel-plugin-tester@<12 will cause babel [to throw][2] since duplicate entries are technically not allowed.

Also note that [babelOptions.babelrc][28] and [babelOptions.configFile][29] are set to false by default, which disables automatic babel configuration loading. [This can be re-enabled if desired][30].

To simply reuse your project's [babel.config.js][31] or some other configuration file, set babelOptions like so:

// file: /repos/my-project/tests/unit-plugin.test.ts

import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';

pluginTester({
  plugin: yourPlugin,
  // ...
  babelOptions: require(path.join('..', 'babel.config.js')),
  // ...
  tests: {
    /* Your test objects */
  }
});
Custom Plugin and Preset Run Order

By default, when you include a custom list of [plugins][32] or [presets][3] 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][33]:

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][17], the first enclosing all [tests][34] (i.e. describe(title, ...)) and the second enclosing all [fixtures][35] (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][15]/[presetName][13].

filepath

This is used to resolve relative paths provided by the [fixtures][35] option; the test object properties [codeFixture][36], [outputFixture][37], and [execFixture][38]; and [during configuration resolution for prettier][39]. That is: if the aforesaid properties are not absolute paths, they will be [path.join][40]'d with the [directory name][41] 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][42].

This option defaults to the absolute path of the file that [invoked the pluginTester function][43].

[!NOTE]

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

[!NOTE]

When disabling line ending conversion, note that [Babel will always output LF][44] 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][45] function. It can also return a promise. If that promise resolves to a function, that will be treated as a [teardown][45] function.

See [here][46] 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][47] 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][47]. See [here][46] for the complete run order.

formatResult

This function is used to format all babel outputs, and defaults to a function that invokes [prettier][48]. If a prettier configuration file is [found][49], then that will be used. Otherwise, prettier will use its own default configuration.

You can also [override or entirely disable formatting][39].

snapshot

Equivalent to [snapshot][50] but applied globally across all [test objects][42].

fixtureOutputName

Equivalent to [fixtureOutputName][51] but applied globally across all [fixtures][35].

fixtureOutputExt

Equivalent to [fixtureOutputExt][52] but applied globally across all [fixtures][35].

titleNumbering

Determines which test titles are prefixed with a number when registering [test blocks][18] (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][43] of babel-plugin-tester in the same test file will share the same [test title numbering][53]. For example:

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from 'universe: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][18] 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:

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from 'universe: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][18] 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][34] option to provide one or more [test objects][42] 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)

[!TIP]

.babelrc, .babelrc.json, .babelrc.js, .babelrc.cjs, and .babelrc.mjs config files in fixture directories are supported out-of-the-box.

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')
});

[!NOTE]

If fixtures is not an absolute path, it will be [path.join][40]'d with the [directory name][41] of [filepath][54].

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][56] will have the same file extension suffix (i.e. .js in code.test.js) as this file unless changed with the [fixtureOutputExt][52] option.

After being transformed by babel, the resulting output will have whitespace trimmed, line endings [converted][57], and then get [formatted by prettier][39].

Note that this file cannot appear in the same directory as [exec.js][58]. 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][59] transformed by babel and [formatted with prettier][39]. If this file is missing and neither [throws][60] nor [exec.js][58] 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][51] and [fixtureOutputExt][52] options.

Before being compared to babel's output, this file's contents will have whitespace trimmed and line endings [converted][57].

Note that this file cannot appear in the same directory as [exec.js][58].

exec.js

This file's contents will be used as the input into babel at transform time just like the [code.js][59] file, except the output will be evaluated in the [same CJS context][61] 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][62] actually throws, your exec.js file might contain:

expect(() => throw new Error('throw expression')).toThrow('throw expression');

[!CAUTION]

Keep in mind that, despite sharing a global context, execution will occur in a [separate realm][63], 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 use JSON.stringify + toBe (or your testing framework's equivalent):

expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
  JSON.stringify({ data: 'imported' })
);

Or use isEqual (or your testing framework's equivalent):

expect(require(`${__dirname}/imported-file.json`)).toEqual({
  data: 'imported'
});

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings [converted][57], and then get [formatted by prettier][39].

Note that this file cannot appear in the same directory as [code.js][59] or [output.js][56]. 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][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][64]. 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][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][lodash.mergewith]) those from the [babelOptions][65] option provided to babel-plugin-tester.

Note that arrays will be concatenated, explicitly undefined values will unset previously defined values, and (as of babel-plugin-tester@>=12) duplicate plugin/preset configurations will override each other (last configuration wins) during merging.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override ([lodash.mergeWith][lodash.mergewith]) those from the [pluginOptions][16] 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][24]) in your options files.

presetOptions

This is used to pass options into your preset at transform time. Properties specified here override ([lodash.mergeWith][lodash.mergewith]) those from the [presetOptions][14] 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][22]) 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.

[!IMPORTANT]

Requires [Jest][66], an equivalent interface (like [Vitest][8]), or a manually-defined it object exposing an appropriate [only][67] 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.

[!IMPORTANT]

Requires [Jest][66], an equivalent interface (like [Vitest][8]), or a manually-defined it object exposing an appropriate [skip][68] method.

throws

[!IMPORTANT]

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, // Should be an instance of this class
  throws: err => {
    if (err instanceof SyntaxError && /message/.test(err.message)) {
      return true; // Test will fail if this function's return value !== true
    }
  },
}

[!CAUTION]

Be careful using instanceof [across realms][69] as it can lead to [strange behavior][70] with [frontend frames/windows][71] and with tools that rely on [Node's VM module][72] (like Jest).

If the value of throws is a class, that class must [be a subtype of Error][77] or the behavior of babel-plugin-tester is undefined.

Note that this property cannot be present when using an [exec.js][58] or [output.js][56] file or when using the [outputRaw][78] option.

[!NOTE]

For backwards compatibility reasons, throws is synonymous with error. They can be used interchangeably, with throws taking precedence.

setup

[!IMPORTANT]

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][79] function. It can also return a promise. If that promise resolves to a function, that will be treated as a [teardown][79] function.

This function, if provided, will run after any [setup][47] function provided as a babel-plugin-tester option. See [here][46] for the complete run order.

teardown

[!IMPORTANT]

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][80] 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][80] property, both of which will run before any [teardown][45] function provided as a babel-plugin-tester option. See [here][46] for the complete run order.

formatResult

[!IMPORTANT]

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][48]. If a prettier configuration file is [found][49], then that will be used. Otherwise, prettier will use its own default configuration.

You can also [entirely disable formatting][39].

This will override the [formatResult][81] function provided to babel-plugin-tester.

outputRaw

[!WARNING]

This feature is only available in babel-plugin-tester@>=12.

[!IMPORTANT]

As it requires a function value, this property must be used in options.js instead of options.json.

This option is similar in intent to [output.js][56] except it tests against the entire [BabelFileResult][82] object returned by [babel's transform function][83] instead of only the code property of [BabelFileResult][82].

outputRaw must be a function with the following signature:

outputRaw: (output: BabelFileResult) => void

Where the output parameter is an instance of [BabelFileResult][82]:

interface BabelFileResult {
  ast?: Node | undefined;
  code?: string | undefined;
  ignored?: boolean | undefined;
  map?: object | undefined;
  metadata?: BabelFileMetadata | undefined;
}

So long as the outputRaw function does not throw, it will never cause the test to fail. On the other hand, if the outputRaw function throws, such as when expect(output.metadata).toStrictEqual({ ... }) fails, the test will fail regardless of other options.

The output parameter is not trimmed, converted, stripped, or modified at all.

Note that outputRaw does not replace [output.js][56] etc, it only adds additional (custom) expectations to your test. Further note that this option can appear alongside any other [fixtures][35] option except [throws][84].

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][59]'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][35] 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][42] 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][15]/[presetName][13].

See [the example][85] for more details.

Test Objects

A minimal test object can be:

  1. A string representing [code][86].
  2. An object with a [code][86] 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][lodash.mergewith]) those from the [babelOptions][65] option provided to babel-plugin-tester.

Note that arrays will be concatenated, explicitly undefined values will unset previously defined values, and (as of babel-plugin-tester@>=12) duplicate plugin/preset configurations will override each other (last configuration wins) during merging.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override ([lodash.mergeWith][lodash.mergewith]) those from the [pluginOptions][16] 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][23]) in your test objects.

presetOptions

This is used to pass options into your preset at transform time. Properties specified here override ([lodash.mergeWith][lodash.mergewith]) those from the [presetOptions][14] 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][21]) 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.

[!IMPORTANT]

Requires [Jest][66], an equivalent interface (like [Vitest][8]), or a manually-defined it object exposing an appropriate [only][67] 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.

[!IMPORTANT]

Requires [Jest][66], an equivalent interface (like [Vitest][8]), or a manually-defined it object exposing an appropriate [skip][68] 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, // Should be an instance of this class
  throws: err => {
    if (err instanceof SyntaxError && /message/.test(err.message)) {
      return true; // Test will fail if this function's return value !== true
    }
  },
}

[!CAUTION]

Be careful using instanceof [across realms][69] as it can lead to [strange behavior][70] with [frontend frames/windows][71] and with tools that rely on [Node's VM module][72] (like Jest).

If the value of throws is a class, that class must [be a subtype of Error][77] or the behavior of babel-plugin-tester is undefined.

Note that this property cannot be present when using the [output][87], [outputRaw][88], [outputFixture][37], [exec][89], [execFixture][38], or [snapshot][50] properties.

[!NOTE]

For backwards compatibility reasons, throws is synonymous with error. They can be used interchangeably, with throws taking precedence.

setup

This function will be run before a particular test is run. It can return a function which will be treated as a [teardown][90] function. It can also return a promise. If that promise resolves to a function, that will be treated as a [teardown][90] function.

This function, if provided, will run after any [setup][47] function provided as a babel-plugin-tester option. See [here][46] 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][91] 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][91] property, both of which will run before any [teardown][45] function provided as a babel-plugin-tester option. See [here][46] for the complete run order.

formatResult

This function is used to format all babel outputs, and defaults to a function that invokes [prettier][48]. If a prettier configuration file is [found][49], then that will be used. Otherwise, prettier will use its own default configuration.

You can also [entirely disable formatting][39].

This will override the [formatResult][81] 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][86] and babel's output.

Defaults to false.

Note that this property cannot appear in the same test object as the [output][87], [outputFixture][37], [exec][89], [execFixture][38], or [throws][84] properties. However, it can be used with [outputRaw][88].

[!IMPORTANT]

Requires [Jest][66], an [appropriate shim][92] or equivalent interface (like [Vitest][8]), or a manually-defined expect object exposing an appropriate [toMatchSnapshot][93] 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][36] or [exec][89] properties instead. If you do not provide the [output][87] or [outputFixture][37] properties, and [snapshot][50] 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][57], and then get [formatted by prettier][39].

Note that this property cannot appear in the same test object as the [codeFixture][36], [exec][89], or [execFixture][38] properties.

output

The value of this property will be compared with the output from [babel's transform function][83].

Before being compared to babel's output, this value will have whitespace trimmed, line endings [converted][57], and any indentation stripped as a convenience for template literals.

Note that this property cannot appear in the same test object as the [outputFixture][37], [exec][89], [execFixture][38], [throws][84], or [snapshot][50] properties. However, it can be used with [outputRaw][88].

outputRaw

[!WARNING]

This feature is only available in babel-plugin-tester@>=12.

This property is similar to [output][87] and related properties except it tests against the entire [BabelFileResult][82] object returned by [babel's transform function][83] instead of only the code property of [BabelFileResult][82].

outputRaw must be a function with the following signature:

outputRaw: (output: BabelFileResult) => void

Where the output parameter is an instance of [BabelFileResult][82]:

interface BabelFileResult {
  ast?: Node | undefined;
  code?: string | undefined;
  ignored?: boolean | undefined;
  map?: object | undefined;
  metadata?: BabelFileMetadata | undefined;
}

So long as the outputRaw function does not throw, this property will never cause the test to fail. On the other hand, if the outputRaw function throws, such as when expect(output.metadata).toStrictEqual({ ... }) fails, the test will fail regardless of other properties.

The output parameter is not trimmed, converted, stripped, or modified at all.

Note that outputRaw does not replace [output][87] etc, it only adds additional (custom) expectations to your test. Further note that outputRaw can appear in the same test object as any other property except [throws][84].

exec

The provided source will be transformed just like the [code][86] property, except the output will be evaluated in the [same CJS context][61] 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][54]), 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][62] actually throws using the following:

{
  // ...
  exec: `
    expect(() => throw new Error('throw expression')).toThrow('throw expression');
  `;
}

[!CAUTION]

Keep in mind that, despite sharing a global context, execution will occur in a [separate realm][63], 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 use JSON.stringify + toBe (or your testing framework's equivalent):

expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
  JSON.stringify({ data: 'imported' })
);

Or use isEqual (or your testing framework's equivalent):

expect(require(`${__dirname}/imported-file.json`)).toEqual({
  data: 'imported'
});

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings [converted][57], and then get [formatted by prettier][39].

Note that this property cannot appear in the same test object as the [execFixture][38], [code][86], [codeFixture][36], [output][87], [outputFixture][37], [throws][84], or [snapshot][50] properties. However, it can be used with [outputRaw][88].

codeFixture

If you would rather put your [code][86] 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][40]'d with the [directory name][41] of [filepath][54].

After being transformed by babel, the resulting output will have whitespace trimmed, line endings [converted][57], and then get [formatted by prettier][39].

Like [code][86], this property cannot appear in the same test object as the [exec][89] or [execFixture][38] properties, nor the [code][86] property.

[!TIP]

If you find you are using this property more than a couple of times, consider using [fixtures][35] instead.

[!NOTE]

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][35], the latter being plural.

outputFixture

If you would rather put your [output][87] 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][40]'d with the [directory name][41] of [filepath][54].

Before being compared to babel's output, this file's contents will have whitespace trimmed and line endings [converted][57].

Like [output][87], this property cannot appear in the same test object as the [exec][89], [execFixture][38], [throws][84], or [snapshot][50] properties, nor the [output][87] property. However, it can be used with [outputRaw][88].

[!TIP]

If you find you are using this property more than a couple of times, consider using [fixtures][35] instead.

execFixture

If you would rather put your [exec][89] 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][40]'d with the [directory name][41] of [filepath][54].

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings [converted][57], and then get [formatted by prettier][39].

Like [exec][89], this property cannot appear in the same test object as the [code][86], [codeFixture][36], [output][87], [outputFixture][37], [throws][84], or [snapshot][50] properties, nor the [exec][89] property. However, it can be used with [outputRaw][88].

[!TIP]

If you find you are using this property more than a couple of times, consider using [fixtures][35] instead.

Examples

Simple Example

import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';

// NOTE: you can use beforeAll, afterAll, beforeEach, and afterEach as usual,
// but initial configuration tasks, like loading content from fixture files,
// will complete *at the point the pluginTester function is called* which means
// BEFORE beforeAll and other Jest hooks are run.

pluginTester({
  plugin: identifierReversePlugin,
  // Defaults to false, but with this line we set the default to true across
  // *all* tests.
  snapshot: true,
  tests: [
    {
      code: "'hello';"
      // Snapshot should show that prettier has changed the single quotes to
      // double quotes (using prettier's default configuration).
    },
    {
      // This test will pass if and only if code has not changed.
      code: '"hello";'
      // To prevent false negatives (like with reckless use of `npx jest -u`),
      // snapshots of code that does not change are forbidden. Snapshots
      // succeed only when babel output !== code input.
      snapshot: false;
    },
    {
      code: 'var hello = "hi";',
      output: 'var olleh = "hi";',
      // You can't take a snapshot and also manually specify an output string.
      // It's either one or the other.
      snapshot: false
    },
    // A valid test can be a test object or a simple string.
    `
      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({
  // One (and ONLY ONE) of the two following lines MUST be included.
  plugin: identifierReversePlugin,
  //preset: coolNewBabelPreset,

  // Usually unnecessary if it is returned by 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 are using fixtures, codeFixture, outputFixture, or
  // execFixture options. Defaults to the absolute path of the file the
  // pluginTester function was invoked from, which in this case  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
  },

  // Defaults to false but we're being explicit here: do not use snapshots
  // across all tests. Note that snapshots are only guaranteed to work with
  // Jest.
  snapshot: false,

  // Defaults to a function that formats with prettier.
  formatResult: customFormatFunction,

  // You can provide tests as an object:
  tests: {
    // The key is the title. The value is the code that is unchanged (because
    // snapshot === false across all tests). 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";'
    }
  },

  // Alternatively, you can provide tests as an array:
  tests: [
    // Should be unchanged by the plugin (because snapshot === false across all
    // tests). 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.
      codeFixture: path.join(
        __dirname,
        '..',
        'fixtures',
        'codeFixture-unchanging.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.
      codeFixture: path.join('..', 'fixtures', 'codeFixture.js'),
      // Because outputFixture is provided, the assertion will be that the
      // plugin will change the contents of "codeFixture.js" to the contents of
      // "outputFixture.js".
      outputFixture: path.join('..', 'fixtures', 'outputFixture.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, overwriting the default/global
      // settings (set above). The snapshot will contain both source code and
      // the transformed output, making 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
      }
      // This can be used to overwrite presetOptions (set above).
      //presetOptions: {
      //  optionB: true
      //}
    },
    {
      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 CJS context as the test
      // runner. This lets us 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");
      `
    }
  ]
});

Fixtures Examples

See [fixtures][35] for an example directory layout or check out the use of babel-plugin-tester fixtures in some of these other projects:

  • [babel-plugin-transform-rewrite-imports][94]
  • [babel-plugin-explicit-exports-references][95]
  • [babel-plugin-transform-default-named-imports][96]

Appendix

Further documentation can be found under [docs/][x-repo-docs].

Testing Framework Compatibility

This package was originally tested on and built to work with [Jest][4], but it is also [tested][97] against [Vitest][8], [Mocha][5], [Jasmine][6], and [node:test][7]. 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 does not 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][98].

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.skip = ... (weirdly, this is already defined)
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][28] and [babelOptions.configFile][29] are set to false by default. This way, you can [manually import (or provide an object literal)][65] the exact configuration you want to apply rather than relying on babel's [somewhat complex configuration loading rules][99]. 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][100], you can leverage [babel's automatic configuration loading][101] via the babelOptions.babelrc and/or babelOptions.configFile options.

[!TIP]

Fixtures provided via the [fixtures][35] option do not need to provide a separate babelOptions.filename since it will be set automatically. This section only applies to [test objects][42].

When relying on babelOptions.babelrc, you must also provide a [babelOptions.filename][102] for each test object that does not include a [codeFixture][36] or [execFixture][38] property. For example:

pluginTester({
  plugin,
  tests: [
    {
      code: '"blah"',
      // This configuration is set at the test level
      babelOptions: {
        babelrc: true,
        filename: path.join(__dirname, 'some-file.js')
      }
    },
    {
      code: '"hi"',
      // This configuration is set at the test level
      babelOptions: {
        babelrc: true,
        filename: path.join(__dirname, 'some-other-file.js')
      }
    },
    {
      // babelOptions.filename will be set to the value of codeFixture for you
      // unless you set it manually here at the test level
      codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
    },
    {
      // babelOptions.filename will be set to the value of execFixture for you
      // unless you set it manually here at the test level
      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][103] properly. Hence, the above example could be simplified further:

pluginTester({
  plugin,
  // This configuration is global: it applies to *all* tests by default!
  babelOptions: {
    babelrc: true,
    // The value of filename does not have to point to a file that exists
    filename: __filename
  },
  tests: [
    '"blah"',
    '"hi"',
    {
      // babelOptions.filename will be set to the value of codeFixture for you
      // unless you set it manually here at the test level
      codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
    },
    {
      // babelOptions.filename will be set to the value of execFixture for you
      // unless you set it manually here at the test level
      execFixture: path.join(__dirname, 'fixtures', 'my-script.js')
    }
  ]
});

pluginName Inference Caveat

Inferring [pluginName][15] during testing requires invoking [the plugin][12] 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][104].

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!

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][81] is used which formats all babel output with [prettier][48]. It will [look for][49] a prettier configuration file relative to [the file that's being tested][54] or the [current working directory][105]. 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][106] 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][107] 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][108] when running your tests.

For example:

# Those using Windows (but not WSL) have to set environment variable differently
NODE_ENV='test' DEBUG='babel-plugin-tester,babel-plugin-tester:*' DEBUG_DEPTH='1' npx jest

Available Debug Namespaces

The following [debug namespaces][109] 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
      • babel-plugin-tester:tester:finalize:order
      • babel-plugin-tester:tester:finalize:duplicates

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:t

changelog

Changelog

All notable changes to this project will be documented in this auto-generated file. The format is based on Conventional Commits; this project adheres to Semantic Versioning.


babel-plugin-tester@12.0.0 (2025-06-14)

💥 BREAKING CHANGES 💥

  • Minimum supported Node.js version is now 20.18.0

  • Default exports are no longer available. See usage instructions for details on proper import syntax.

  • ResultFormatter is no longer synchronous and can now return a Promise.

  • Adoption of prettier\@3 requires some versions of Node to be executed with the --experimental-vm-modules option. E.g. NODE_OPTIONS="--no-warnings --experimental-vm-modules" npx jest.

  • Attempting to install babel-plugin-tester alongside jest@<30 will cause NPM to fail with `ERESOLVE`. This is because only jest@>=30 (jest-snapshot) supports the prettier\@3 asynchronous interface.

✨ Features

  • Allow testing entire babel transform result via outputRaw (e9d5aa1) see #186
  • Support collapsing/overwriting technically-illegal duplicate plugin/preset PluginItems (bf0a088)
  • Upgrade to prettier\@3 (3334248)

🪄 Fixes

  • readme: use proper codecov coverage badge (1047e5c)

⚙️ Build System

  • Adopt @-xun/symbiote (89ec951)
  • deps: bump core-js from 3.40.0 to 3.42.0 (879dd6b)
  • deps: bump core-js from 3.42.0 to 3.43.0 (7590b87)
  • deps: bump debug from 4.4.0 to 4.4.1 (3c2538c)
  • deps: bump prettier from 3.4.2 to 3.5.3 (e0a29da)
  • deps: bump pretty-format from 29.7.0 to 30.0.0 (7ea13c3)
  • deps: bump rejoinder from 1.2.5 to 2.0.1 (76dcccd)
  • deps: bump type-fest from 4.32.0 to 4.41.0 (bcbb568)
  • package: be more selective about which files are included during docs generation (cbb4215)
  • package: bump minimum supported node versions to maintained (ee6f7da)
  • release: add "master" branch to release branches (03734ea)
  • Upgrade typescript-babel toolchain to nodenext (d0b722f)

🧙🏿 Refactored

  • src: deprecated default exports are no longer available (b02d4f0)


babel-plugin-tester@11.0.0 (2023-01-18)

💥 BREAKING CHANGES 💥

  • error no longer accepts arbitrary class constructors

    error (aka throws) no longer accepts arbitrary class constructors. Any provided class constructor must extend Error, e.g. built-ins like SyntaxError or custom error classes like class MyError extends Error. Thanks to the nature of JavaScript, **providing a class constructor that does not extend Error will lead to undefined behavior**.

  • error only captures exceptions from Babel

    error (aka throws) no longer potentially captures exceptions thrown by the formatResult function. If the formatResult function throws, the entire test will fail immediately.

  • TypeError for config error; AssertionError for test error

    All configuration-related issues now throw TypeError instead of AssertionError. AssertionError is now exclusively used for failing tests. Additionally, the text of some error messages has been updated.

  • All test titles are now numbered

    All test titles are now numbered (e.g. "1. ...", "2. ...", etc), including fixtures tests and tests with custom titles.

  • Built-in TypeScript support

    TypeScript types are now included within the package itself, obviating the need to install a separate types package. Installing the old types package alongside this version of babel-plugin-tester will cause conflicts.

  • Fixture configuration schema is standardized

    In previous versions of babel-plugin-tester, you could provide any key to options.json and it would be passed as-is to the plugin under test. This made it impossible to allow fixtures to be configured with the same flexibility as test objects. In this version of babel-plugin-tester, fixture options.json (and options.js) files must return a standard set of options. Non-standard properties are silently ignored. For instance: to pass options to the plugin under test, they must be provided via pluginOptions.

  • Global describe and it functions must be defined

    Babel-plugin-tester will refuse to run if describe, it, it.only, or it.skip are not globally available.

  • Global setup/teardown no longer overwrites local versions

    In previous versions of babel-plugin-tester, test-level setup and teardown functions overrode global setup and teardown functions. In this version of babel-plugin-tester, the global setup and teardown functions will be called alongside their test-level counterparts for each test and in a well-defined order (see documentation).

  • Implicit "global" options merging is no longer supported

    In previous versions of babel-plugin-tester, any test object and fixture configuration option could be passed directly to babel-plugin-tester and apply "globally" across all test objects and fixtures. This was even the case for options that made no sense in a "global" context, such as only, skip, and code. In this version of babel-plugin-tester, only options explicitly listed in the documentation can be passed directly and applied globally. Unrecognized "rest" options are silently ignored.

  • Test/fixture configuration is resolved early and consistently

    In previous versions of babel-plugin-tester, test object and fixture configuration options were resolved in various places, with some options getting finalized before it(...) and describe(...) were called and others being determined as Jest was executing the test. In this version, all configuration options are resolved and finalized before it(...) and describe(...) are called. This also means configurations are finalized _before_ hooks like beforeAll get called by the testing framework.

  • babelOptions.filename is now set to filepath by default rather than undefined.

  • In previous versions, the lodash.mergeWith customizer skipped source properties that resolved to undefined. With this version, the customizer now unsets these properties (sets them to undefined), allowing the end user to easily unset defaults (e.g. filename).

  • Minimum recommended node version bumped from 10.13.0 to 14.20.0

  • Plugin names are once again automatically determined by analyzing the return value of the plugin function. Though this is implemented in a backwards-compatible way, there is a [small caveat](https://github.com/babel-utils/babel-plugin-tester#pluginname-inference-caveat).

✨ Features

  • Add support for testing presets (73b90b3)
  • Implement default filepath inference using Error stack trace (9d1b321)
  • src: add exec/execFixture support via Node's VM module (4754f42)
  • src: add support for "only", "skip", and "title" test options in fixtures (#90) (89b58b5)
  • src: add support for arbitrary run order of plugin under test (#91) (8c8b858)
  • src: add support for loading prettier configuration files in fixtures (f54deda)
  • src: add TEST_SKIP/TEST_NUM_SKIP/TEST_ONLY/TEST_NUM_ONLY env variable support (13626d1)
  • src: bring back (lightweight) plugin name inference (#92) (f9ad903)
  • src: implement titleNumbering and restartTitleNumbering options (09e792d)
  • src: implement standard setup/teardown run order (4ea283f)
  • src: provide debug output support via debug package (4c7c6e7)
  • Windows support (f214995)

🪄 Fixes

  • src: ensure test function errors are not swallowed by teardown function errors (2acfe37)
  • src: fix fixtureOutputExt being ignored in root options.json (#89) (481be19)
  • src: fix plugin run order for fixtures to match tests (#88) (fbb6c19)

⚙️ Build System

  • deps: bump prettier from 2.8.0 to 2.8.1 (#98) (0bdb351)
  • package: restore @babel/core\@7.11.6 as minimum supported version (00712c0)
  • Transmute codebase to TypeScript (#96) (5f588e9)
  • Update tooling (d5b4d9c)

🧙🏿 Refactored

  • Lodash.mergeWith customizer now allows unsetting options by setting them to undefined (74af680)
  • Reorganize source into unified extensible tester pipeline w/ first-class fixtures support (0c44392)


🏗️ Patch babel-plugin-tester@11.0.4 (2023-01-25)

🪄 Fixes

  • Ensure exec realm has access to context-sensitive versions of __filename and __dirname globals (0306698)


🏗️ Patch babel-plugin-tester@11.0.3 (2023-01-24)

🪄 Fixes

  • Pass full file path to prettier::resolveConfig, not just the dirname (e9ebcdd)


🏗️ Patch babel-plugin-tester@11.0.2 (2023-01-23)

🪄 Fixes

  • src: use cross-realm symbols (330aa1e)
  • Use node-util import compatible with node\@14 (2c4cd84)

⚙️ Build System

  • babel: explicitly include polyfills for shipped proposals (850d58c)


🏗️ Patch babel-plugin-tester@11.0.1 (2023-01-18)

🪄 Fixes

  • src: ensure deprecated config option is still supported by prettierFormatter (e48badf) see #139


babel-plugin-tester@10.1.0 (2021-05-29)

✨ Features

  • Use babel.transformAsync when it's available (#84) (969be11)


babel-plugin-tester@10.0.0 (2020-10-02)

💥 BREAKING CHANGES 💥

  • Plugin name inference is no longer supported

🪄 Fixes

  • pluginnameinference: remove the inference. Default to "unknown plugin" (#78) (91c22ec) see #60


babel-plugin-tester@9.2.0 (2020-05-27)

✨ Features

  • Add 'fixtureOutputExt' configuration option (#73) (ae67eee)


babel-plugin-tester@9.1.0 (2020-05-20)

✨ Features


babel-plugin-tester@9.0.0 (2020-03-23)

💥 BREAKING CHANGES 💥

  • Requires Node >= 10.13.0

🪄 Fixes

  • node: update to prettier\@2 and drop Node < 10.13 (#66) (1e7ad06)


🏗️ Patch babel-plugin-tester@9.0.1 (2020-04-10)

🪄 Fixes

  • Apply fix line endings to code and output in two spots (#68) (f1c17ef)


babel-plugin-tester@8.0.0 (2019-11-15)

💥 BREAKING CHANGES 💥

  • Your snapshots will probably need to be updated with these changes. If you don't like the changes, then take a look at the README about overriding the formatResult and disabling the un-string snapshot serializer

✨ Features

  • Format result with prettier and unstring snapshots (#55) (60e5c07)


🏗️ Patch babel-plugin-tester@8.0.1 (2019-12-01)

🪄 Fixes

  • Avoid crash when importing without global expect (#56) (a134785)


babel-plugin-tester@7.0.0 (2019-08-19)

💥 BREAKING CHANGES 💥

  • Require Node 8

✨ Features


🏗️ Patch babel-plugin-tester@7.0.4 (2019-11-14)

🪄 Fixes

  • Set configFile to false by default (7b97a6f)


🏗️ Patch babel-plugin-tester@7.0.3 (2019-11-11)

🪄 Fixes


🏗️ Patch babel-plugin-tester@7.0.2 (2019-11-09)

🪄 Fixes


🏗️ Patch babel-plugin-tester@7.0.1 (2019-08-19)

🪄 Fixes


babel-plugin-tester@6.5.0 (2019-08-18)

✨ Features


babel-plugin-tester@6.4.0 (2019-06-12)

✨ Features

  • Control which linefeed character to use (#45) (8c38231)


babel-plugin-tester@6.3.0 (2019-06-12)

✨ Features


🏗️ Patch babel-plugin-tester@6.3.1 (2019-06-12)

🪄 Fixes


babel-plugin-tester@6.2.0 (2019-05-12)

✨ Features

  • fixtures: get plugin options for fixtures from options.json (#41) (7b9e76d)


🏗️ Patch babel-plugin-tester@6.2.1 (2019-05-12)

🪄 Fixes

  • fixtures: get options from root options.json (#42) (556ca0d)


babel-plugin-tester@6.1.0 (2019-05-04)

✨ Features

  • fixtures: allow formatting fixtures results (#39) (e6c219f)


babel-plugin-tester@6.0.0 (2019-02-14)

💥 BREAKING CHANGES 💥

  • This upgrades to babel 7. You'll need to use babel 7 as well.

✨ Features


🏗️ Patch babel-plugin-tester@6.0.1 (2019-03-14)

🪄 Fixes

  • Avoid returning values in describe blocks (#38) (f3d7b5b)


babel-plugin-tester@5.5.0 (2018-08-05)

✨ Features

  • Add typescript file extension support (#32) (c8e49be)


🏗️ Patch babel-plugin-tester@5.5.2 (2018-11-17)

🪄 Fixes


🏗️ Patch babel-plugin-tester@5.5.1 (2018-08-17)

🪄 Fixes

  • Change babel-core import to optional require in default param (#33) (2b33b36)


babel-plugin-tester@5.4.0 (2018-06-10)

✨ Features


babel-plugin-tester@5.3.0 (2018-06-04)

✨ Features

  • Create automatically fixtures output.js files for new tests (#27) (d48a8fc)


babel-plugin-tester@5.2.0 (2018-06-04)

✨ Features

  • Accept fixtureOutputName option for fixtures mode (#26) (f3e1ad2)


babel-plugin-tester@5.1.0 (2018-06-02)

✨ Features

  • Provide your own implementation of babel (#25) (cb230ec)


babel-plugin-tester@5.0.0 (2017-11-25)

💥 BREAKING CHANGES 💥

  • You'll have to install babel-core yourself now.

🪄 Fixes

  • Move babel-core to peerDependencies & devDependencies (#20) (46c70d1)


babel-plugin-tester@4.0.0 (2017-08-16)

💥 BREAKING CHANGES 💥

  • Your snapshots will break (for the better). Just update them. Nothing else changed.

✨ Features

  • test-numbers: only use numbers for tests without titles (#19) (6cd36d1)


babel-plugin-tester@3.3.0 (2017-07-18)

✨ Features

  • formatresult: add formatResult option (#17) (6085c16)


babel-plugin-tester@3.2.0 (2017-06-30)

✨ Features

  • tests: add setup/teardown functions (#14) (9b30ca3)


🏗️ Patch babel-plugin-tester@3.2.2 (2017-07-06)

🪄 Fixes


🏗️ Patch babel-plugin-tester@3.2.1 (2017-07-05)

🪄 Fixes

  • Assert should be (actual, expected) (9fcb418)


babel-plugin-tester@3.1.0 (2017-06-12)

✨ Features

  • Add ability to pass plugin options (#13) (d7aa18a)


babel-plugin-tester@3.0.0 (2017-05-23)

💥 BREAKING CHANGES 💥

  • Default parser options changed to remove recast

🪄 Fixes

  • release: manually release a major version (c78460b) see #11


babel-plugin-tester@2.0.0 (2017-05-19)

💥 BREAKING CHANGES 💥

  • fixtures has been repurposed. See the docs.

  • modifier has been removed, use only and skip instead

✨ Features

  • errors: add errors config (#5) (c157316)
  • fixtures: add fixtures directory (#6) (6e1554d)
  • tests: add only and skip, remove modifier (#7) (ad1d1b1)


babel-plugin-tester@1.1.0 (2017-05-16)

✨ Features


🏗️ Patch babel-plugin-tester@1.1.1 (2017-05-16)

🪄 Fixes

  • Properly deindent and trim code and output (eb60549)


babel-plugin-tester@1.0.0 (2017-05-16)

✨ Features