Skip to content

MichaelDeBoey/babel-plugin-tester

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

84 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

babel-plugin-tester

Utilities for testing babel plugins and presets.


Build Status Code Coverage Version Downloads MIT License

All Contributors

PRs Welcome Code of Conduct

The Problem

You're writing a babel plugin or preset and want to write tests for it too.

This Solution

This is a fairly simple abstraction to help you write tests for your babel plugin or preset. It was built to work with Jest, but most of the functionality should work with Mocha, Jasmine, and any other framework that defines standard it/describe/expect globals.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

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

Usage

import

ESM:

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

CJS:

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

For backwards compatibility reasons, a default export is also available but its use should be avoided.

Invoke

/* 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 any test/it/describe block.

Options

This section lists the options you can pass to babel-plugin-tester. They are all optional with respect to the following:

plugin

This is used to provide the babel plugin under test. For example:

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

pluginName

This is used as the describe block name and in your tests' names. If pluginName can be inferred from the plugin's name, then it will be and you 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.

pluginOptions

This is used to pass options into your plugin at transform time. If provided, the object will be lodash.mergewith'd with each test object's pluginOptions/fixture's pluginOptions, with the latter taking precedence.

preset

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] };
}

presetName

This is used as the describe block name and in your tests' names. Defaults to "unknown preset".

presetOptions

This is used to pass options into your preset at transform time. This option can be overridden using test object properties or fixture options.

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 included in this package.

babelOptions

This is used to configure babel. If provided, the object will be lodash.mergewith'd with the defaults and each test object's babelOptions/fixture's babelOptions, with the latter taking precedence.

Note that 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.

title

This is used to specify a custom title for the describe block (overriding everything else).

filepath

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

fixtures

There are two ways to create tests: using the tests option to provide one or more test objects or using the fixtures option described here. Both can be used simultaneously.

The fixtures option must be a path to a directory with a structure similar to the following:

__fixtures__
β”œβ”€β”€ first-test         # test title will be: "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 be path.join'd with the directory name of filepath.

And it would run four tests, one for each directory in __fixtures__.

code.js

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.

output.js

This file, if provided, will have its contents compared with babel's output, which is code.js transformed by babel and formatted with prettier. 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.

exec.js

This file's contents will be used as the input into babel at transform time just like the code.js file, except the output will be evaluated in the same 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.

options.json (Or options.js)

For each fixture, the contents of the entirely optional options.json file are lodash.mergewith'd with the options provided to babel-plugin-tester, with the former taking precedence. 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:

babelOptions

This is used to configure babel. Properties specified here override (lodash.mergewith) those from the babelOptions option provided to babel-plugin-tester.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override (lodash.mergewith) those from the pluginOptions option provided to babel-plugin-tester.

title

If provided, this will be used as the title of the test (overriding the directory name).

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.

skip

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.

throws

When using certain values, this option must be used in options.js instead of options.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 with error. They can be used interchangeably.

Note that this property is ignored when using an exec.js file.

setup

As it requires a function value, this option must be used in options.js instead of options.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.

teardown

As it requires a function value, this option must be used in options.js instead of options.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.

formatResult

As it requires a function value, this option must be used in options.js instead of options.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.

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. This is particularly useful if you are testing TypeScript input. If omitted, the fixture's input file extension will be used instead.

tests

There are two ways to create tests: using the fixtures option that leverages the filesystem or using the tests option described here. Both can be used simultaneously.

Using the tests option, you can provide test objects describing your expected transformations. You can provide tests as an object of test objects or an array of test objects. If you provide an object, the object's keys will be used as the default title of each test. If you provide an array, each test's default title will be derived from its index and pluginName/presetName.

See the example for more details.

Test Objects

A minimal test object can be:

  1. A string representing code.
  2. An object with a code property.

Here are the available properties if you provide an object:

babelOptions

This is used to configure babel. Properties specified here override (lodash.mergewith) those from the babelOptions option provided to babel-plugin-tester.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override (lodash.mergewith) those from the pluginOptions option provided to babel-plugin-tester.

title

If provided, this will be used as the title of the test (overriding everything else).

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.

skip

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.

throws

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 with error. They can be used interchangeably.

Note that this property is ignored when using the exec property.

setup

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.

teardown

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.

formatResult

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.

snapshot

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.

code

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.

output

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.

codeFixture

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 with fixture. They can be used interchangeably, though care must be taken not to confuse the test object option fixture with the babel-plugin-tester option fixtures, the latter being plural.

outputFixture

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.

exec

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.

Unrecognized Options

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.

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

pluginTester({
  plugin: identifierReversePlugin,
  snapshot: true,
  tests: [
    {
      code: '"hello";',
      snapshot: false
    },
    {
      code: 'var hello = "hi";',
      output: 'var olleh = "hi";'
    },
    `
      function sayHi(person) {
        return 'Hello ' + person + '!'
      }
      console.log(sayHi('Jenny'))
    `
  ]
});

Full Example

import { 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");
      `
    }
  ]
});

Documentation

Using Babel for Configuration Loading

babelOptions.babelrc and babelOptions.configFile are set to false by default. This way, you can manually import (or provide an object literal) the exact configuration you want to apply rather than relying on babel's somewhat complex configuration loading rules. However, if your plugin, preset, or project relies on a complicated external setup to do its work, and you 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 a filename.

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

pluginName Inference Caveat

Inferring pluginName during testing requires invoking the plugin at least twice: once outside of babel to check for the plugin's name and then again when run by babel. This is irrelevant to babel-plugin-tester (even if your plugin crashes when run outside of babel) and to the overwhelming majority of babel plugins in existence. This only becomes a problem if your plugin is aggressively stateful, which is against the babel handbook on plugin design.

For example, the following plugin which replaces an import specifier using a regular expression will exhibit strange behavior due to being invoked twice:

/*  -*-*-  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'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'

Formatting Output with Prettier

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
  // ...
});

Built-In Debugging Support

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.

Available Debug Namespaces

  • babel-plugin-tester:index
  • babel-plugin-tester:tester
  • babel-plugin-tester:formatter
  • babel-plugin-tester:serializer

TEST_ONLY and TEST_SKIP Environment Variables

The optional TEST_ONLY and TEST_SKIP environment variables are recognized by babel-plugin-tester, allowing you to control which tests are run in an adhoc fashion without 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.

Inspiration

The API was inspired by:

Issues

Looking to contribute? Look for the Good First Issue label.

πŸ› Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

πŸ’‘ Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a πŸ‘. This helps maintainers prioritize what to work on.

See Feature Requests

Contributors ✨

Thanks goes to these people (emoji key):


Kent C. Dodds

πŸ’» πŸ“– πŸš‡ ⚠️

james kyle

πŸ’» πŸ“– πŸ‘€ ⚠️

Brad Bohen

πŸ›

Kyle Welch

πŸ’» πŸ“– ⚠️

kontrollanten

πŸ’»

RubΓ©n Norte

πŸ’» ⚠️

AndrΓ© Neves

πŸ’» ⚠️

Kristoffer K.

πŸ’» ⚠️

Alex Kanunnikov

πŸ’» ⚠️

Sebastian Silbermann

πŸ’»

Andrey Los

πŸ›

Charles Bodman

πŸ“–

MichaΓ«l De Boey

πŸ’»

yuyaryshev

πŸ’»

Marek Buchar

πŸ’» ⚠️ πŸ“–

Jay Phelps

πŸ‘€

Mathias

πŸ“–

joe moon

πŸ’» ⚠️

Bernard

πŸ’» ⚠️ πŸ“–
πŸš‡ πŸ‘€ 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!

License

MIT

About

Utilities for testing babel plugins

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 84.0%
  • JavaScript 15.7%
  • Shell 0.3%