babel-plugin-tester
Version:
Utilities for testing babel plugins
1,159 lines (1,158 loc) • 47.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.pluginTester = pluginTester;
exports.prettierFormatter = void 0;
exports.restartTestTitleNumbering = restartTestTitleNumbering;
exports.validTitleNumberingValues = exports.validEndOfLineValues = exports.unstringSnapshotSerializer = exports.runPresetUnderTestHere = exports.runPluginUnderTestHere = void 0;
require("core-js/modules/es.array.push.js");
require("core-js/modules/es.iterator.constructor.js");
require("core-js/modules/es.iterator.filter.js");
require("core-js/modules/es.iterator.find.js");
require("core-js/modules/es.iterator.for-each.js");
require("core-js/modules/es.iterator.map.js");
require("core-js/modules/es.iterator.some.js");
var _nodeAssert = _interopRequireDefault(require("node:assert"));
var _nodeFs = _interopRequireDefault(require("node:fs"));
var _nodeOs = require("node:os");
var _nodeUtil = require("node:util");
var _nodeVm = require("node:vm");
var _fs = require("@-xun/fs");
var _lodash = _interopRequireDefault(require("lodash.mergewith"));
var _stripIndent = _interopRequireDefault(require("strip-indent~3"));
var _constant = require("./constant.js");
var _errors = require("./errors.js");
var _prettier = require("./formatters/prettier.js");
exports.prettierFormatter = _prettier.prettierFormatter;
var _unstringSnapshot = require("./serializers/unstring-snapshot.js");
exports.unstringSnapshotSerializer = _unstringSnapshot.unstringSnapshotSerializer;
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const {
isNativeError
} = _nodeUtil.types;
const parseErrorStackRegExp = /at (?:(?<fn>\S+) )?(?:.*? )?\(?(?<path>(?:\/|file:|\w:\\).*?)(?:\)|$)/i;
const parseScriptFilepathRegExp = /(\/|\\)babel-plugin-tester(\/|\\)((dist|src)(\/|\\))+(index|plugin-tester)\.(j|t)s$/;
const isIntegerRegExp = /^\d+$/;
const isIntegerRangeRegExp = /^(?<startStr>\d+)-(?<endStr>\d+)$/;
const noop = () => undefined;
Object.freeze(noop);
const getDebuggers = (namespace, parentDebugger) => {
const debug = parentDebugger.extend(namespace);
return {
debug,
verbose: debug.extend('verbose')
};
};
const {
debug: debug1,
verbose: verbose1
} = getDebuggers('tester', _constant.globalDebugger);
const runPluginUnderTestHere = exports.runPluginUnderTestHere = Symbol.for('@xunnamius/run-plugin-under-test-here');
const runPresetUnderTestHere = exports.runPresetUnderTestHere = Symbol.for('@xunnamius/run-preset-under-test-here');
const validTitleNumberingValues = exports.validTitleNumberingValues = ['all', 'tests-only', 'fixtures-only', false];
const validEndOfLineValues = exports.validEndOfLineValues = ['lf', 'crlf', 'auto', 'preserve', false];
let currentTestNumber = 1;
function restartTestTitleNumbering() {
debug1('restarted title numbering');
currentTestNumber = 1;
}
function pluginTester(options = {}) {
debug1('executing main babel-plugin-tester function');
const globalContextHasExpectFn = 'expect' in globalThis && typeof expect === 'function';
const globalContextHasTestFn = 'it' in globalThis && typeof it === 'function';
const globalContextHasDescribeFn = 'describe' in globalThis && typeof describe === 'function';
const globalContextExpectFnHasToMatchSnapshot = (() => {
try {
return globalContextHasExpectFn ? typeof expect(undefined).toMatchSnapshot === 'function' : false;
} catch {
return false;
}
})();
const globalContextTestFnHasSkip = globalContextHasTestFn ? typeof it.skip === 'function' : false;
const globalContextTestFnHasOnly = globalContextHasTestFn ? typeof it.only === 'function' : false;
if (!globalContextHasDescribeFn) {
throw new TypeError(_errors.ErrorMessage.TestEnvironmentUndefinedDescribe());
}
if (!globalContextHasTestFn) {
throw new TypeError(_errors.ErrorMessage.TestEnvironmentUndefinedIt());
}
debug1('global context check succeeded');
let hasTests = false;
const baseConfig = resolveBaseConfig();
const envConfig = resolveConfigFromEnvironmentVariables();
const normalizedTests = normalizeTests();
verbose1('base configuration: %O', baseConfig);
verbose1('environment-derived config: %O', envConfig);
verbose1('normalized test blocks: %O', normalizedTests);
if (!hasTests) {
debug1('terminated early: no valid tests provided');
return;
}
registerTestsWithTestingFramework(normalizedTests);
debug1('finished registering all test blocks with testing framework');
debug1('finished executing main babel-plugin-tester function');
function resolveBaseConfig() {
const {
debug: debug2,
verbose: verbose2
} = getDebuggers('resolve-base', debug1);
debug2('resolving base configuration');
const rawBaseConfig = (0, _lodash.default)({
babelOptions: {
parserOpts: {},
generatorOpts: {},
babelrc: false,
configFile: false
},
titleNumbering: 'all',
endOfLine: 'lf',
formatResult: r => r,
snapshot: false,
fixtureOutputName: 'output',
setup: noop,
teardown: noop
}, options, mergeCustomizer);
verbose2('raw base configuration: %O', rawBaseConfig);
if (rawBaseConfig.plugin && (rawBaseConfig.preset || rawBaseConfig.presetName || rawBaseConfig.presetOptions) || rawBaseConfig.preset && (rawBaseConfig.plugin || rawBaseConfig.pluginName || rawBaseConfig.pluginOptions)) {
throw new TypeError(_errors.ErrorMessage.BadConfigPluginAndPreset());
}
if (!validTitleNumberingValues.includes(rawBaseConfig.titleNumbering)) {
throw new TypeError(_errors.ErrorMessage.BadConfigInvalidTitleNumbering());
}
const baseConfig = {
babel: rawBaseConfig.babel || require('@babel/core'),
baseBabelOptions: rawBaseConfig.babelOptions,
titleNumbering: rawBaseConfig.titleNumbering,
filepath: rawBaseConfig.filepath || rawBaseConfig.filename || tryInferFilepath(),
endOfLine: rawBaseConfig.endOfLine,
baseSetup: rawBaseConfig.setup,
baseTeardown: rawBaseConfig.teardown,
baseFormatResult: rawBaseConfig.formatResult,
baseSnapshot: rawBaseConfig.snapshot,
baseFixtureOutputName: rawBaseConfig.fixtureOutputName,
baseFixtureOutputExt: rawBaseConfig.fixtureOutputExt,
fixtures: rawBaseConfig.fixtures,
tests: rawBaseConfig.tests || []
};
verbose2('partially constructed base configuration: %O', baseConfig);
if (baseConfig.fixtures !== undefined && typeof baseConfig.fixtures !== 'string') {
throw new TypeError(_errors.ErrorMessage.BadConfigFixturesNotString());
}
if (!Array.isArray(baseConfig.tests) && typeof baseConfig.tests !== 'object') {
throw new TypeError(_errors.ErrorMessage.BadConfigInvalidTestsType());
}
baseConfig.tests = Array.isArray(baseConfig.tests) ? baseConfig.tests.filter((test, ndx) => {
if (Array.isArray(test) || typeof test !== 'string' && test !== null && test !== undefined && typeof test !== 'object') {
throw new TypeError(_errors.ErrorMessage.BadConfigInvalidTestsArrayItemType(ndx));
}
const result = typeof test === 'string' || Boolean(test);
if (!result) {
debug2(`test item \`%O\` at index ${ndx} was skipped`, test);
}
return result;
}) : Object.fromEntries(Object.entries(baseConfig.tests).filter(([title, test]) => {
if (Array.isArray(test) || typeof test !== 'string' && test !== null && test !== undefined && typeof test !== 'object') {
throw new TypeError(_errors.ErrorMessage.BadConfigInvalidTestsObjectProperty(title));
}
const result = typeof test === 'string' || Boolean(test);
if (!result) {
debug2(`test property "${title}" with value \`%O\` was skipped`, test);
}
return result;
}));
if (rawBaseConfig.plugin) {
debug2('running in plugin mode');
baseConfig.plugin = rawBaseConfig.plugin;
baseConfig.pluginName = rawBaseConfig.pluginName || tryInferPluginName() || 'unknown plugin';
baseConfig.basePluginOptions = rawBaseConfig.pluginOptions || {};
} else if (rawBaseConfig.preset) {
debug2('running in preset mode');
baseConfig.preset = rawBaseConfig.preset;
baseConfig.presetName = rawBaseConfig.presetName || 'unknown preset';
baseConfig.basePresetOptions = rawBaseConfig.presetOptions;
} else {
throw new TypeError(_errors.ErrorMessage.BadConfigNoPluginOrPreset());
}
baseConfig.describeBlockTitle = rawBaseConfig.title === false ? false : rawBaseConfig.title || baseConfig.pluginName || baseConfig.presetName || undefined;
debug2('describe block title: %O', baseConfig.describeBlockTitle);
if (rawBaseConfig.restartTitleNumbering) {
restartTestTitleNumbering();
}
return baseConfig;
function tryInferPluginName() {
debug2('attempting to infer plugin name');
try {
const {
name
} = rawBaseConfig.plugin({
assertVersion: noop,
targets: noop,
assumption: noop
}, {}, process.cwd());
debug2('plugin name inference result: %O', name);
return name;
} catch {
debug2('plugin name inference failed');
return undefined;
}
}
function tryInferFilepath() {
if ('filepath' in rawBaseConfig || 'filename' in rawBaseConfig) {
debug2('filepath was manually unset');
return undefined;
}
debug2('attempting to infer filepath');
const oldStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = Number.POSITIVE_INFINITY;
try {
let inferredFilepath = undefined;
const reversedCallStack = (new Error('faux error').stack?.split('\n').map(line => {
const {
fn: functionName,
path: filePath
} = line.match(parseErrorStackRegExp)?.groups || {};
return filePath ? {
functionName,
filePath: filePath.split(`file://${process.platform === 'win32' ? '/' : ''}`).at(-1).split(':').slice(0, -2).join(':')
} : undefined;
}).filter(o => Boolean(o)) || []).reverse();
verbose2('reversed call stack: %O', reversedCallStack);
if (reversedCallStack.length) {
const referenceIndex = findReferenceStackIndex(reversedCallStack);
verbose2('reference index: %O', referenceIndex);
if (referenceIndex) {
inferredFilepath = reversedCallStack.at(referenceIndex - 1)?.filePath;
}
}
debug2('inferred filepath: %O', inferredFilepath);
return inferredFilepath;
} finally {
Error.stackTraceLimit = oldStackTraceLimit;
}
function findReferenceStackIndex(reversedCallStack) {
return [reversedCallStack.findIndex(({
functionName,
filePath
}) => {
return functionName === 'defaultPluginTester' && parseScriptFilepathRegExp.test(filePath);
}), reversedCallStack.findIndex(({
functionName,
filePath
}) => {
return functionName === 'pluginTester' && parseScriptFilepathRegExp.test(filePath);
}), reversedCallStack.findIndex(({
functionName,
filePath
}) => {
return functionName === 'resolveBaseConfig' && parseScriptFilepathRegExp.test(filePath);
})].find(index => index !== -1);
}
}
}
function resolveConfigFromEnvironmentVariables() {
const {
debug: debug2
} = getDebuggers('resolve-env', debug1);
debug2('resolving environment variable configuration');
return {
skipTestsByRegExp: stringToRegExp(process.env.TEST_SKIP),
onlyTestsByRegExp: stringToRegExp(process.env.TEST_ONLY),
skipTestsByRange: stringToRanges('TEST_NUM_SKIP', process.env.TEST_NUM_SKIP),
onlyTestsByRange: stringToRanges('TEST_NUM_ONLY', process.env.TEST_NUM_ONLY)
};
function stringToRegExp(str) {
return str === undefined ? undefined : new RegExp(str, 'u');
}
function stringToRanges(name, str) {
if (typeof str !== 'string') {
return [];
}
return str.split(',').map(s => {
s = s.trim();
if (s) {
if (isIntegerRegExp.test(s)) {
return Number(s);
}
const {
startStr,
endStr
} = s.match(isIntegerRangeRegExp)?.groups || {};
if (startStr && endStr) {
const start = Number(startStr);
const end = Number(endStr);
const range = {
start,
end
};
if (start > end) {
throw new TypeError(_errors.ErrorMessage.BadEnvironmentVariableRange(name, s, range));
} else if (start === end) {
return start;
}
return range;
}
throw new TypeError(_errors.ErrorMessage.BadEnvironmentVariableRange(name, s));
}
}).filter(s => Boolean(s));
}
}
function normalizeTests() {
const {
debug: debug2
} = getDebuggers('normalize', debug1);
debug2('normalizing test items into test objects');
const {
describeBlockTitle,
filepath,
tests,
fixtures
} = baseConfig;
const testsIsArray = Array.isArray(tests);
const fixturesAbsolutePath = getAbsolutePathUsingFilepathDirname(filepath, fixtures);
const testConfigs = [];
const useFixtureTitleNumbering = baseConfig.titleNumbering === 'all' || baseConfig.titleNumbering === 'fixtures-only';
const useTestObjectTitleNumbering = baseConfig.titleNumbering === 'all' || baseConfig.titleNumbering === 'tests-only';
if (fixturesAbsolutePath) {
debug2('potentially generating test objects from fixtures path: %O', fixturesAbsolutePath);
if (_nodeFs.default.statSync(fixturesAbsolutePath).isDirectory()) {
debug2('generating test objects from fixtures path');
const describeBlock = typeof describeBlockTitle === 'string' ? createAndPushDescribeConfig(`${describeBlockTitle} fixtures`) : undefined;
if (describeBlock === undefined) {
debug2('skipped creating describe block');
}
createAndPushFixtureConfigs({
fixturesDirectory: fixturesAbsolutePath,
parentDescribeConfig: describeBlock
});
} else {
debug2('not generating test objects from fixtures path: path is not a directory');
}
} else if (typeof fixtures === 'string') {
throw new TypeError(_errors.ErrorMessage.UnableToDeriveAbsolutePath(filepath, '`filepath`', fixtures, '`fixtures`'));
} else {
debug2('skipped loading fixtures: no fixtures path provided');
}
if (!testsIsArray || tests.length) {
debug2('generating test objects from tests');
const describeBlock = typeof describeBlockTitle === 'string' ? createAndPushDescribeConfig(describeBlockTitle) : undefined;
if (describeBlock === undefined) {
debug2('skipped creating describe block');
}
if (testsIsArray) {
debug2(`${tests.length} tests were provided via an array`);
(describeBlock?.tests || testConfigs).push(...tests.map(test => createTestConfig(test)));
} else {
const entries = Object.entries(tests);
debug2(`${entries.length} tests were provided via an object`);
(describeBlock?.tests || testConfigs).push(...entries.map(([title, test]) => {
return createTestConfig({
title,
...(typeof test === 'string' ? {
code: test
} : test)
});
}));
}
} else {
debug2('skipped loading test objects from tests: no tests object or array provided');
}
debug2('finished normalizing tests');
return testConfigs;
function createAndPushDescribeConfig(describeBlockTitle, parentDescribeConfig) {
const {
debug: debug3
} = getDebuggers('create-desc', debug2);
debug3('generating new describe block: %O', describeBlockTitle);
const describeConfig = {
[_constant.$type]: 'describe-block',
describeBlockTitle,
tests: []
};
(parentDescribeConfig?.tests || testConfigs).push(describeConfig);
return describeConfig;
}
function createAndPushFixtureConfigs({
fixturesDirectory,
fixtureOptions = {},
parentDescribeConfig
}) {
const {
debug: debug3,
verbose: verbose3
} = getDebuggers('create-fix', debug2);
debug3('potentially generating test objects from fixture at path %O', fixturesDirectory);
if (!_nodeFs.default.statSync(fixturesDirectory).isDirectory()) {
debug3('test objects generation skipped: path is not a directory');
return;
}
const rootOptions = (0, _lodash.default)({
setup: noop,
teardown: noop
}, fixtureOptions, readFixtureOptions(fixturesDirectory), mergeCustomizer);
verbose3('root options: %O', rootOptions);
_nodeFs.default.readdirSync(fixturesDirectory).forEach(filename => {
const fixtureSubdir = (0, _fs.toPath)(fixturesDirectory, filename);
debug3('potentially generating new test object from fixture at subpath %O', fixtureSubdir);
if (!_nodeFs.default.statSync(fixtureSubdir).isDirectory()) {
debug3('test object generation skipped: subpath is not a directory');
return;
}
const blockTitle = filename.split('-').join(' ');
const localOptions = (0, _lodash.default)({}, rootOptions, readFixtureOptions(fixtureSubdir), mergeCustomizer);
verbose3('localOptions: %O', localOptions);
const directoryFiles = _nodeFs.default.readdirSync(fixtureSubdir, {
withFileTypes: true
}).filter(file => file.isFile());
const {
name: codeFilename
} = directoryFiles.find(file => {
return file.name.startsWith('code.');
}) || {};
const {
name: execFilename
} = directoryFiles.find(file => {
return file.name.startsWith('exec.');
}) || {};
verbose3('code filename: %O', codeFilename);
verbose3('exec filename: %O', execFilename);
if (!codeFilename && !execFilename) {
debug3('no code or exec file found in subpath. Skipped generating test object. Subpath will be scanned for nested fixtures');
createAndPushFixtureConfigs({
fixturesDirectory: fixtureSubdir,
fixtureOptions: localOptions,
parentDescribeConfig: createAndPushDescribeConfig(blockTitle, parentDescribeConfig)
});
} else {
debug3('code or exec file found in subpath. Skipped scanning for nested fixtures. Test object will be generated');
const codePath = codeFilename ? (0, _fs.toPath)(fixtureSubdir, codeFilename) : undefined;
const execPath = execFilename ? (0, _fs.toPath)(fixtureSubdir, execFilename) : undefined;
const hasBabelrc = ['.babelrc', '.babelrc.json', '.babelrc.js', '.babelrc.cjs', '.babelrc.mjs'].some(p => {
return _nodeFs.default.existsSync((0, _fs.toPath)(fixtureSubdir, p));
});
const {
plugin,
basePluginOptions,
preset,
basePresetOptions,
baseBabelOptions,
endOfLine,
baseFormatResult,
baseFixtureOutputExt,
baseFixtureOutputName
} = baseConfig;
const {
babelOptions,
pluginOptions,
presetOptions,
title,
only,
skip,
throws,
error,
setup,
teardown,
formatResult,
outputRaw,
fixtureOutputName,
fixtureOutputExt
} = localOptions;
const code = readCode(codePath);
const exec = readCode(execPath);
const outputExtension = (fixtureOutputExt || baseFixtureOutputExt || (codeFilename || execFilename).split('.').pop()).replace(/^\./, '');
const fixtureOutputBasename = `${fixtureOutputName || baseFixtureOutputName}.${outputExtension}`;
const outputPath = (0, _fs.toPath)(fixtureSubdir, fixtureOutputBasename);
const hasOutputFile = outputPath && _nodeFs.default.existsSync(outputPath);
const output = hasOutputFile ? trimAndFixLineEndings(readCode(outputPath), endOfLine, code) : undefined;
const testConfig = (0, _lodash.default)({
[_constant.$type]: 'fixture-object'
}, {
babelOptions: baseBabelOptions
}, {
babelOptions: {
filename: codePath || execPath,
babelrc: hasBabelrc
}
}, {
babelOptions: babelOptions || {}
}, {
babelOptions: {
plugins: [],
presets: []
},
testBlockTitle: (() => {
const titleString = title || blockTitle;
if (useFixtureTitleNumbering) {
const numericPrefix = currentTestNumber++;
return {
numericPrefix,
titleString,
fullString: `${numericPrefix}. ${titleString}`
};
} else {
return {
numericPrefix: undefined,
titleString,
fullString: titleString
};
}
})(),
only,
skip,
expectedError: throws ?? error,
testSetup: setup || noop,
testTeardown: teardown || noop,
formatResult: formatResult || baseFormatResult,
fixtureOutputBasename,
code,
codeFixture: codePath,
output,
outputRaw,
outputFixture: outputPath,
exec,
execFixture: execPath
}, mergeCustomizer);
verbose3('partially constructed fixture-based test object: %O', testConfig);
if (plugin) {
testConfig.babelOptions.plugins.push([plugin, (0, _lodash.default)({}, basePluginOptions, pluginOptions, mergeCustomizer)]);
} else {
testConfig.babelOptions.presets.unshift([preset, (0, _lodash.default)({}, basePresetOptions, presetOptions, mergeCustomizer)]);
}
finalizePluginAndPresetRunOrder(testConfig.babelOptions);
reduceDuplicatePluginsAndPresets(testConfig.babelOptions);
verbose3('finalized fixture-based test object: %O', testConfig);
validateTestConfig(testConfig);
hasTests = true;
(parentDescribeConfig?.tests || testConfigs).push(testConfig);
}
});
}
function createTestConfig(testObject) {
const {
verbose: verbose3
} = getDebuggers('create-obj', debug2);
verbose3('generating new test object');
if (typeof testObject === 'string') {
testObject = {
code: testObject
};
}
verbose3('raw test object: %O', testObject);
const {
plugin,
pluginName,
basePluginOptions,
preset,
presetName,
basePresetOptions,
baseBabelOptions,
endOfLine,
baseFormatResult,
baseSnapshot
} = baseConfig;
const {
babelOptions,
pluginOptions,
presetOptions,
title,
only,
skip,
throws,
error,
setup,
teardown,
formatResult,
snapshot,
code: originalCode,
output: originalOutput,
outputRaw,
exec: originalExec,
fixture,
codeFixture: originalCodeFixture,
outputFixture,
execFixture: originalExecFixture
} = (0, _lodash.default)({
setup: noop,
teardown: noop
}, testObject, mergeCustomizer);
const codeFixture = getAbsolutePathUsingFilepathDirname(filepath, originalCodeFixture ?? fixture);
const execFixture = getAbsolutePathUsingFilepathDirname(filepath, originalExecFixture);
const code = originalCode !== undefined ? (0, _stripIndent.default)(originalCode) : readCode(codeFixture);
const output = originalOutput !== undefined ? (0, _stripIndent.default)(originalOutput) : readCode(filepath, outputFixture);
const exec = originalExec ?? readCode(execFixture);
const testConfig = (0, _lodash.default)({
[_constant.$type]: 'test-object'
}, {
babelOptions: baseBabelOptions
}, {
babelOptions: {
filename: codeFixture || execFixture || filepath || baseBabelOptions.filename
}
}, {
babelOptions: babelOptions || {}
}, {
babelOptions: {
plugins: [],
presets: []
},
snapshot: snapshot ?? baseSnapshot,
testBlockTitle: (() => {
const titleString = title || pluginName || presetName;
if (useTestObjectTitleNumbering) {
const numericPrefix = currentTestNumber++;
return {
numericPrefix,
titleString,
fullString: `${numericPrefix}. ${titleString}`
};
} else {
return {
numericPrefix: undefined,
titleString,
fullString: titleString
};
}
})(),
only,
skip,
expectedError: throws ?? error,
testSetup: setup || noop,
testTeardown: teardown || noop,
formatResult: formatResult || baseFormatResult,
code,
codeFixture,
output: output !== undefined ? trimAndFixLineEndings(output, endOfLine, code) : undefined,
outputRaw,
outputFixture,
exec,
execFixture: exec !== undefined ? execFixture || filepath || baseBabelOptions.filename || undefined : undefined
}, mergeCustomizer);
verbose3('partially constructed test object: %O', testConfig);
if (plugin) {
testConfig.babelOptions.plugins.push([plugin, (0, _lodash.default)({}, basePluginOptions, pluginOptions, mergeCustomizer)]);
} else {
testConfig.babelOptions.presets.unshift([preset, (0, _lodash.default)({}, basePresetOptions, presetOptions, mergeCustomizer)]);
}
finalizePluginAndPresetRunOrder(testConfig.babelOptions);
reduceDuplicatePluginsAndPresets(testConfig.babelOptions);
verbose3('finalized test object: %O', testConfig);
validateTestConfig(testConfig, {
hasCodeAndCodeFixture: !!(originalCode && codeFixture),
hasOutputAndOutputFixture: !!(originalOutput && outputFixture),
hasExecAndExecFixture: !!(originalExec && execFixture)
});
hasTests = true;
return testConfig;
}
}
function registerTestsWithTestingFramework(tests) {
const {
debug: debug2
} = getDebuggers('register', debug1);
debug2(`registering ${tests.length} blocks with testing framework`);
tests.forEach(testConfig => {
if (testConfig[_constant.$type] === 'describe-block') {
debug2(`registering describe block "${testConfig.describeBlockTitle}" and its sub-blocks`);
describe(testConfig.describeBlockTitle, () => {
registerTestsWithTestingFramework(testConfig.tests);
});
} else {
const {
skip,
only,
testBlockTitle: {
numericPrefix,
titleString,
fullString
}
} = testConfig;
let method = undefined;
if (envConfig.skipTestsByRegExp?.test(titleString) || numericPrefixInRanges(numericPrefix, envConfig.skipTestsByRange)) {
method = 'skip';
debug2(`registering test block "${fullString}" (with \`skip\` property enabled via environment variable)`);
} else if (envConfig.onlyTestsByRegExp?.test(titleString) || numericPrefixInRanges(numericPrefix, envConfig.onlyTestsByRange)) {
method = 'only';
debug2(`registering test block "${fullString}" (with \`only\` property enabled via environment variable)`);
} else if (skip) {
method = 'skip';
debug2(`registering test block "${fullString}" (with \`skip\` property enabled)`);
} else if (only) {
method = 'only';
debug2(`registering test block "${fullString}" (with \`only\` property enabled)`);
} else {
debug2(`registering test block "${fullString}"`);
}
(method ? it[method] : it)(fullString, frameworkTestWrapper(testConfig));
}
});
}
function frameworkTestWrapper(testConfig) {
const {
verbose: verbose2
} = getDebuggers('wrapper', debug1);
return async () => {
const {
baseSetup,
baseTeardown
} = baseConfig;
const {
testSetup,
testTeardown
} = testConfig;
const setupFunctions = [baseSetup, testSetup];
const teardownFunctions = [testTeardown, baseTeardown];
for (const [index, setupFn] of setupFunctions.entries()) {
verbose2(`running setup function #${index + 1}${setupFn === noop ? ' (noop)' : ''}`);
try {
const maybeTeardownFn = await setupFn();
if (typeof maybeTeardownFn === 'function') {
verbose2(`registered teardown function returned from setup function #${index + 1}`);
teardownFunctions.splice(index - 1, 0, maybeTeardownFn);
}
} catch (error) {
const message = _errors.ErrorMessage.SetupFunctionFailed(error);
verbose2(message);
throw new Error(message, {
cause: error
});
}
}
let frameworkError;
try {
await frameworkTest(testConfig);
} catch (error) {
frameworkError = error;
verbose2('caught framework test failure');
} finally {
for (const [index, teardownFn] of teardownFunctions.entries()) {
verbose2(`running teardown function #${index + 1}${teardownFn === noop ? ' (noop)' : ''}`);
try {
await teardownFn();
} catch (error) {
const message = _errors.ErrorMessage.TeardownFunctionFailed(error, frameworkError);
verbose2(message);
throw new Error(message, {
cause: {
error,
frameworkError
}
});
}
}
if (frameworkError) {
verbose2('rethrowing framework test failure');
throw frameworkError;
}
}
};
}
async function frameworkTest(testConfig) {
const {
debug: debug2,
verbose: verbose2
} = getDebuggers('test', debug1);
const {
babel,
endOfLine,
filepath
} = baseConfig;
const {
babelOptions,
testBlockTitle,
expectedError,
formatResult,
code,
codeFixture,
output,
outputRaw,
outputFixture,
exec,
execFixture
} = testConfig;
debug2(`test framework has triggered test "${testBlockTitle.fullString}"`);
let errored = false;
const rawBabelOutput = await (async () => {
try {
const transformer = babel.transformAsync || babel.transform;
const parameters = [code ?? exec, babelOptions];
verbose2(`calling babel transform function (${transformer.name}) with parameters: %O`, parameters);
return await transformer(...parameters);
} catch (error) {
verbose2(`babel transformation failed with error: ${String(error)}`);
if (expectedError) {
errored = true;
return error;
} else {
throw error;
}
}
})();
try {
if (expectedError) {
debug2('expecting babel transform function to fail with error');
(0, _nodeAssert.default)(errored, _errors.ErrorMessage.ExpectedBabelToThrow());
if (typeof expectedError === 'function') {
if (expectedError === Error || expectedError.prototype instanceof Error) {
(0, _nodeAssert.default)(rawBabelOutput instanceof expectedError, _errors.ErrorMessage.ExpectedErrorToBeInstanceOf(expectedError));
} else if (!expectedError(rawBabelOutput)) {
_nodeAssert.default.fail(_errors.ErrorMessage.ExpectedThrowsFunctionToReturnTrue());
}
} else {
const resultString = isNativeError(rawBabelOutput) ? rawBabelOutput.message : String(rawBabelOutput);
if (typeof expectedError === 'string') {
(0, _nodeAssert.default)(resultString.includes(expectedError), _errors.ErrorMessage.ExpectedErrorToIncludeString(resultString, expectedError));
} else if (expectedError instanceof RegExp) {
(0, _nodeAssert.default)(expectedError.test(resultString), _errors.ErrorMessage.ExpectedErrorToMatchRegExp(resultString, expectedError));
}
}
} else {
debug2('expecting babel transform function to succeed');
const isRawBabelOutput = !!rawBabelOutput && typeof rawBabelOutput === 'object';
const hasRawBabelOutputCode = isRawBabelOutput && 'code' in rawBabelOutput;
if (isRawBabelOutput && outputRaw) {
debug2('executing provided outputRaw function before final comparison');
await outputRaw(rawBabelOutput);
}
if (!hasRawBabelOutputCode || typeof rawBabelOutput.code !== 'string') {
throw new TypeError(_errors.ErrorMessage.BabelOutputTypeIsNotString(hasRawBabelOutputCode ? rawBabelOutput.code : rawBabelOutput));
}
debug2('performing final comparison');
const formatResultFilepath = codeFixture || execFixture || filepath;
const result = trimAndFixLineEndings(await formatResult(rawBabelOutput.code || '', {
cwd: formatResultFilepath ? (0, _fs.toDirname)(formatResultFilepath) : undefined,
filepath: formatResultFilepath,
filename: formatResultFilepath
}), endOfLine, code);
if (exec !== undefined) {
debug2('executing output from babel transform function');
(0, _nodeAssert.default)(result.length > 0, _errors.ErrorMessage.BabelOutputUnexpectedlyEmpty());
const fakeModule = {
exports: {}
};
const context = (0, _nodeVm.createContext)({
...globalThis,
module: fakeModule,
exports: fakeModule.exports,
require,
__dirname: (0, _fs.toDirname)(execFixture),
__filename: execFixture
});
new _nodeVm.Script(result, {
filename: execFixture
}).runInContext(context, {
displayErrors: true,
breakOnSigint: true,
microtaskMode: 'afterEvaluate'
});
} else if (testConfig[_constant.$type] === 'test-object' && testConfig.snapshot) {
debug2('expecting output from babel transform function to match snapshot');
(0, _nodeAssert.default)(result !== code, _errors.ErrorMessage.AttemptedToSnapshotUnmodifiedBabelOutput());
const separator = '\n\n ↓ ↓ ↓ ↓ ↓ ↓\n\n';
const formattedOutput = [code, separator, result].join('');
expect(`\n${formattedOutput}\n`).toMatchSnapshot(testBlockTitle.fullString);
} else if (output !== undefined) {
debug2('expecting output from babel transform function to match expected output');
_nodeAssert.default.equal(result, output, _errors.ErrorMessage.ExpectedOutputToEqualActual(testConfig));
} else if (testConfig[_constant.$type] === 'fixture-object' && outputFixture) {
debug2('writing output from babel transform function to new output file');
_nodeFs.default.writeFileSync(outputFixture, result);
} else {
debug2('expecting output from babel transform function to match input');
_nodeAssert.default.equal(result, trimAndFixLineEndings(code, endOfLine), _errors.ErrorMessage.ExpectedOutputNotToChange());
}
}
} catch (error) {
verbose2('test failed: %O', error);
throw error;
}
}
function validateTestConfig(testConfig, knownViolations) {
const {
verbose: verbose2
} = getDebuggers('validate', debug1);
verbose2('known violations: %O', knownViolations);
const {
testBlockTitle,
skip,
only,
code,
exec,
output,
babelOptions,
expectedError,
outputRaw
} = testConfig;
if (knownViolations) {
const {
hasCodeAndCodeFixture,
hasOutputAndOutputFixture,
hasExecAndExecFixture
} = knownViolations;
if (hasCodeAndCodeFixture) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasCodeAndCodeFixture());
}
if (hasOutputAndOutputFixture) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasOutputAndOutputFixture());
}
if (hasExecAndExecFixture) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasExecAndExecFixture());
}
}
if (testConfig[_constant.$type] === 'test-object' && testConfig.snapshot) {
if (!globalContextExpectFnHasToMatchSnapshot) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.TestEnvironmentNoSnapshotSupport());
}
if (output !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasSnapshotAndOutput());
}
if (exec !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasSnapshotAndExec());
}
if (expectedError !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasSnapshotAndThrows());
}
}
if (skip && only) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasSkipAndOnly());
}
if (skip && !globalContextTestFnHasSkip) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.TestEnvironmentNoSkipSupport());
}
if (only && !globalContextTestFnHasOnly) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.TestEnvironmentNoOnlySupport());
}
if (output !== undefined && expectedError !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasThrowsAndOutput(testConfig));
}
if (outputRaw !== undefined && expectedError !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasThrowsAndOutputRaw());
}
if (exec !== undefined && expectedError !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasThrowsAndExec(testConfig));
}
if (code === undefined && exec === undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidMissingCodeOrExec(testConfig));
}
if ((code !== undefined || output !== undefined) && exec !== undefined) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasExecAndCodeOrOutput(testConfig));
}
if (babelOptions.babelrc && !babelOptions.filename) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidHasBabelrcButNoFilename());
}
if (expectedError !== undefined && !(['function', 'boolean', 'string'].includes(typeof expectedError) || expectedError instanceof RegExp)) {
throwTypeErrorWithDebugOutput(_errors.ErrorMessage.InvalidThrowsType());
}
function throwTypeErrorWithDebugOutput(message) {
const finalMessage = _errors.ErrorMessage.ValidationFailed(testBlockTitle.fullString, message);
verbose2(finalMessage);
throw new TypeError(finalMessage);
}
}
}
function mergeCustomizer(objValue, srcValue, key, object, source) {
if (srcValue === undefined && key in source) {
delete object[key];
} else if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
return undefined;
}
function getAbsolutePathUsingFilepathDirname(filepath, basename) {
const {
verbose: verbose2
} = getDebuggers('to-abs-path', debug1);
const result = !basename ? undefined : (0, _fs.isAbsolutePath)(basename) ? basename : filepath ? (0, _fs.toPath)((0, _fs.toDirname)(filepath), basename) : undefined;
verbose2(`dirname(${String(filepath)}) + ${String(basename)} => ${String(result)}`);
return result;
}
function readFixtureOptions(baseDirectory) {
const {
verbose: verbose2
} = getDebuggers('read-opts', debug1);
const optionsPath = [(0, _fs.toPath)(baseDirectory, 'options.js'), (0, _fs.toPath)(baseDirectory, 'options.json')].find(p => _nodeFs.default.existsSync(p));
try {
if (optionsPath) {
verbose2(`requiring options file ${optionsPath}`);
return require(optionsPath);
} else {
verbose2('attempt to require options file ignored: no such file exists');
return {};
}
} catch (error) {
const message = _errors.ErrorMessage.GenericErrorWithPath(error, optionsPath);
verbose2(`attempt to require options file failed: ${message}`);
throw new Error(message);
}
}
function readCode(filepath, basename) {
const {
verbose: verbose2
} = getDebuggers('read-code', debug1);
const codePath = arguments.length === 1 ? filepath : getAbsolutePathUsingFilepathDirname(filepath, basename);
if (!codePath) {
verbose2(`attempt to read in contents from file ignored: no absolute path derivable from filepath "${String(filepath)}" and basename "${String(basename)}"`);
return undefined;
}
if (!(0, _fs.isAbsolutePath)(codePath)) {
const message = _errors.ErrorMessage.PathIsNotAbsolute(codePath);
verbose2(`attempt to read in contents from file failed: ${message}`);
throw new Error(message);
}
try {
verbose2(`reading in contents from file ${codePath}`);
return _nodeFs.default.readFileSync(codePath, 'utf8');
} catch (error) {
const message = _errors.ErrorMessage.GenericErrorWithPath(error, codePath);
verbose2(`attempt to read in contents from file failed: ${message}`);
throw new Error(message);
}
}
function trimAndFixLineEndings(source, endOfLine, input = source) {
const {
verbose: verbose2
} = getDebuggers('eol', debug1);
source = source.trim();
if (endOfLine === false) {
verbose2('no EOL fix applied: EOL conversion disabled');
return source;
}
verbose2(`applying EOL fix "${endOfLine}": all EOL will be replaced`);
verbose2('input (trimmed) with original EOL: %O', source.replaceAll('\r', String.raw`\r`).replaceAll('\n', String.raw`\n`));
const output = source.replaceAll(/\r?\n/g, getReplacement()).trim();
verbose2('output (trimmed) with EOL fix applied: %O', output.replaceAll('\r', String.raw`\r`).replaceAll('\n', String.raw`\n`));
return output;
function getReplacement() {
switch (endOfLine) {
case 'lf':
{
return '\n';
}
case 'crlf':
{
return '\r\n';
}
case 'auto':
{
return _nodeOs.EOL;
}
case 'preserve':
{
const match = input.match(/\r?\n/);
if (match === null) {
return _nodeOs.EOL;
}
return match[0];
}
default:
{
verbose2(`encountered invalid EOL option "${String(endOfLine)}"`);
throw new TypeError(_errors.ErrorMessage.BadConfigInvalidEndOfLine(endOfLine));
}
}
}
}
function finalizePluginAndPresetRunOrder(babelOptions) {
const {
verbose: verbose2
} = getDebuggers('finalize:order', debug1);
if (babelOptions?.plugins) {
babelOptions.plugins = babelOptions.plugins.filter(p => {
const result = Boolean(p);
if (!result) {
verbose2('a falsy `babelOptions.plugins` item was filtered out');
}
return result;
});
if (babelOptions.plugins.includes(runPluginUnderTestHere)) {
verbose2('replacing `runPluginUnderTestHere` symbol in `babelOptions.plugins` with plugin under test');
babelOptions.plugins.splice(babelOptions.plugins.indexOf(runPluginUnderTestHere), 1, babelOptions.plugins.pop());
}
}
if (babelOptions?.presets) {
babelOptions.presets = babelOptions.presets.filter(p => {
const result = Boolean(p);
if (!result) {
verbose2('a falsy `babelOptions.presets` item was filtered out');
}
return result;
});
if (babelOptions.presets.includes(runPresetUnderTestHere)) {
verbose2('replacing `runPresetUnderTestHere` symbol in `babelOptions.presets` with preset under test');
babelOptions.presets.splice(babelOptions.presets.indexOf(runPresetUnderTestHere) - 1, 1, babelOptions.presets.shift());
}
}
verbose2('finalized test object plugin and preset run order');
}
function reduceDuplicatePluginsAndPresets(babelOptions) {
const {
verbose: verbose2
} = getDebuggers('finalize:duplicates', debug1);
if (babelOptions?.plugins) {
const plugins = [];
babelOptions.plugins.forEach(incomingPlugin => {
if (incomingPlugin && typeof incomingPlugin !== 'symbol') {
const incomingPluginName = pluginOrPresetToName(incomingPlugin);
if (incomingPluginName) {
const duplicatedPluginIndex = plugins.findIndex(outgoingPlugin => {
if (outgoingPlugin && typeof outgoingPlugin !== 'symbol') {
const outgoingPluginName = pluginOrPresetToName(outgoingPlugin);
return outgoingPluginName === incomingPluginName;
}
});
if (duplicatedPluginIndex !== -1) {
verbose2('collapsed duplicate plugin configuration for %O (at index %O)', incomingPluginName, duplicatedPluginIndex);
plugins[duplicatedPluginIndex] = incomingPlugin;
return;
}
}
}
plugins.push(incomingPlugin);
});
babelOptions.plugins = plugins;
}
if (babelOptions?.presets) {
const presets = [];
babelOptions.presets.forEach(incomingPreset => {
if (incomingPreset && typeof incomingPreset !== 'symbol') {
const incomingPresetName = pluginOrPresetToName(incomingPreset);
if (incomingPresetName) {
const duplicatedPresetIndex = presets.findIndex(outgoingPreset => {
if (outgoingPreset && typeof outgoingPreset !== 'symbol') {
const outgoingPresetName = pluginOrPresetToName(outgoingPreset);
return outgoingPresetName === incomingPresetName;
}
});
if (duplicatedPresetIndex !== -1) {
verbose2('collapsed duplicate preset configuration for %O (at index %O)', incomingPresetName, duplicatedPresetIndex);
presets[duplicatedPresetIndex] = incomingPreset;
return;
}
}
}
presets.push(incomingPreset);
});
babelOptions.presets = presets;
}
verbose2('collapsed duplicate test object plugins and presets');
function pluginOrPresetToName(pluginOrPreset) {
if (typeof pluginOrPreset === 'string') {
return pluginOrPreset;
}
if (Array.isArray(pluginOrPreset)) {
const candidate = pluginOrPreset.at(2) ?? pluginOrPreset.at(0);
return typeof candidate === 'string' ? candidate : undefined;
}
if (typeof pluginOrPreset === 'object' && 'name' in pluginOrPreset) {
return typeof pluginOrPreset.name === 'string' ? pluginOrPreset.name : undefined;
}
return undefined;
}
}
function numericPrefixInRanges(numericPrefix, ranges) {
if (typeof numericPrefix === 'number') {
return ranges.some(range => {
return typeof range === 'number' ? numericPrefix === range : numericPrefix >= range.start && numericPrefix <= range.end;
});
}
return false;
}