UNPKG

babel-plugin-tester

Version:
1,470 lines (1,141 loc) โ€ข 93.9 kB
<!-- symbiote-template-region-start 1 --> <p align="center" width="100%"> <img width="300" src="./logo.png"> </p> <p align="center" width="100%"> <!-- symbiote-template-region-end --> Utilities for testing babel plugins ๐Ÿงช <!-- symbiote-template-region-start 2 --> </p> <hr /> <div align="center"> [![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] </div> <br /> # babel-plugin-tester <!-- symbiote-template-region-end --> 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. <!-- symbiote-template-region-start 3 --> --- <!-- remark-ignore-start --> <!-- symbiote-template-region-end --> <!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> - [Install](#install) - [Usage](#usage) - [Import](#import) - [Invoke](#invoke) - [Execute](#execute) - [Configure](#configure) - [Examples](#examples) - [Simple Example](#simple-example) - [Full Example](#full-example) - [Fixtures Examples](#fixtures-examples) - [Appendix](#appendix) - [Testing Framework Compatibility](#testing-framework-compatibility) - [Using Babel for Configuration Loading](#using-babel-for-configuration-loading) - [`pluginName` Inference Caveat](#pluginname-inference-caveat) - [Custom Snapshot Serialization](#custom-snapshot-serialization) - [Formatting Output with Prettier](#formatting-output-with-prettier) - [Built-In Debugging Support](#built-in-debugging-support) - [`TEST_ONLY`/`TEST_NUM_ONLY` and `TEST_SKIP`/`TEST_NUM_SKIP` Environment Variables](#test_onlytest_num_only-and-test_skiptest_num_skip-environment-variables) - [`setup` and `teardown` Run Order](#setup-and-teardown-run-order) - [Published Package Details](#published-package-details) - [License](#license) - [Contributing and Support](#contributing-and-support) - [Inspiration](#inspiration) - [Contributors](#contributors) <!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- symbiote-template-region-start 4 --> <!-- remark-ignore-end --> <br /> ## Install <!-- symbiote-template-region-end --> ```shell 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: ```javascript import { pluginTester } from 'babel-plugin-tester'; ``` CJS: ```javascript const { pluginTester } = require('babel-plugin-tester'); ``` ### Invoke ```javascript /* 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: ```shell # 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: ```javascript /* 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: ```javascript /* 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: ```javascript // 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: ```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 (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: ```javascript 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: ```javascript 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]: ```javascript 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: ```javascript /* 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: ```text 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: ```javascript /* 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: ```text 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: ```text 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: ```javascript 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: ```javascript 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: > > ```javascript > 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: > > ```javascript > expect( > Object.fromEntries( > Object.entries(require(`${__dirname}/imported-file.json`)) > ) > ).toStrictEqual({ data: 'imported' }); > ``` > > Or use `JSON.stringify` + `toBe` (or your testing framework's equivalent): > > ```javascript > expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe( > JSON.stringify({ data: 'imported' }) > ); > ``` > > Or use `isEqual` (or your testing framework's equivalent): > > ```javascript > 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: ```javascript { // ... 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: ```typescript outputRaw: (output: BabelFileResult) => void ``` Where the `output` parameter is an instance of [`BabelFileResult`][82]: ```typescript 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: ```javascript { // ... 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: ```typescript outputRaw: (output: BabelFileResult) => void ``` Where the `output` parameter is an instance of [`BabelFileResult`][82]: ```typescript 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: ```javascript { // ... 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: > > ```javascript > 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: > > ```javascript > expect( > Object.fromEntries( > Object.entries(require(`${__dirname}/imported-file.json`)) > ) > ).toStrictEqual({ data: 'imported' }); > ``` > > Or use `JSON.stringify` + `toBe` (or your testing framework's equivalent): > > ```javascript > expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe( > JSON.stringify({ data: 'imported' }) > ); > ``` > > Or use `isEqual` (or your testing framework's equivalent): > > ```javascript > 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 ```javascript 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 ```javascript 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 === f