UNPKG

babel-plugin-tester

Version:
1,159 lines (1,158 loc) 47.8 kB
"use strict"; 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; }