updated `jest` and `babel-jest` to `v23` fixed breaking change in `jest`: - https://github.com/facebook/jest/blob/master/CHANGELOG.md#2300 - https://github.com/facebook/jest/pull/5558 _edit:_ forgot to mention. test runner fix is based on: https://github.com/babel/babel/blob/master/packages/babel-parser/test/helpers/runFixtureTests.js#L11
391 lines
10 KiB
JavaScript
391 lines
10 KiB
JavaScript
/* eslint-env jest */
|
|
import * as babel from "@babel/core";
|
|
import { buildExternalHelpers } from "@babel/core";
|
|
import getFixtures from "@babel/helper-fixtures";
|
|
import sourceMap from "source-map";
|
|
import { codeFrameColumns } from "@babel/code-frame";
|
|
import defaults from "lodash/defaults";
|
|
import includes from "lodash/includes";
|
|
import * as helpers from "./helpers";
|
|
import extend from "lodash/extend";
|
|
import merge from "lodash/merge";
|
|
import resolve from "resolve";
|
|
import assert from "assert";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import vm from "vm";
|
|
|
|
import diff from "jest-diff";
|
|
|
|
const moduleCache = {};
|
|
const testContext = vm.createContext({
|
|
...helpers,
|
|
process: process,
|
|
transform: babel.transform,
|
|
setTimeout: setTimeout,
|
|
setImmediate: setImmediate,
|
|
expect,
|
|
});
|
|
testContext.global = testContext;
|
|
|
|
// Initialize the test context with the polyfill, and then freeze the global to prevent implicit
|
|
// global creation in tests, which could cause things to bleed between tests.
|
|
runModuleInTestContext("@babel/polyfill", __filename);
|
|
|
|
// Populate the "babelHelpers" global with Babel's helper utilities.
|
|
runCodeInTestContext(buildExternalHelpers(), {
|
|
filename: path.join(__dirname, "babel-helpers-in-memory.js"),
|
|
});
|
|
|
|
/**
|
|
* A basic implementation of CommonJS so we can execute `@babel/polyfill` inside our test context.
|
|
* This allows us to run our unittests
|
|
*/
|
|
function runModuleInTestContext(id: string, relativeFilename: string) {
|
|
const filename = resolve.sync(id, {
|
|
basedir: path.dirname(relativeFilename),
|
|
});
|
|
|
|
// Expose Node-internal modules if the tests want them. Note, this will not execute inside
|
|
// the context's global scope.
|
|
if (filename === id) return require(id);
|
|
|
|
if (moduleCache[filename]) return moduleCache[filename].exports;
|
|
|
|
const module = (moduleCache[filename] = {
|
|
id: filename,
|
|
exports: {},
|
|
});
|
|
const dirname = path.dirname(filename);
|
|
const req = id => runModuleInTestContext(id, filename);
|
|
|
|
const src = fs.readFileSync(filename, "utf8");
|
|
const code = `(function (exports, require, module, __filename, __dirname) {${src}\n});`;
|
|
|
|
vm.runInContext(code, testContext, {
|
|
filename,
|
|
displayErrors: true,
|
|
}).call(module.exports, module.exports, req, module, filename, dirname);
|
|
|
|
return module.exports;
|
|
}
|
|
|
|
/**
|
|
* Run the given snippet of code inside a CommonJS module.
|
|
*
|
|
* Exposed for unit tests, not for use as an API.
|
|
*/
|
|
export function runCodeInTestContext(code: string, opts: { filename: string }) {
|
|
const filename = opts.filename;
|
|
const dirname = path.dirname(filename);
|
|
const req = id => runModuleInTestContext(id, filename);
|
|
|
|
const module = {
|
|
id: filename,
|
|
exports: {},
|
|
};
|
|
|
|
const oldCwd = process.cwd();
|
|
try {
|
|
if (opts.filename) process.chdir(path.dirname(opts.filename));
|
|
|
|
// Expose the test options as "opts", but otherwise run the test in a CommonJS-like environment.
|
|
// Note: This isn't doing .call(module.exports, ...) because some of our tests currently
|
|
// rely on 'this === global'.
|
|
const src = `(function(exports, require, module, __filename, __dirname, opts) {${code}\n});`;
|
|
return vm.runInContext(src, testContext, {
|
|
filename,
|
|
displayErrors: true,
|
|
})(module.exports, req, module, filename, dirname, opts);
|
|
} finally {
|
|
process.chdir(oldCwd);
|
|
}
|
|
}
|
|
|
|
function wrapPackagesArray(type, names, optionsDir) {
|
|
return (names || []).map(function(val) {
|
|
if (typeof val === "string") val = [val];
|
|
|
|
// relative path (outside of monorepo)
|
|
if (val[0][0] === ".") {
|
|
if (!optionsDir) {
|
|
throw new Error(
|
|
"Please provide an options.json in test dir when using a " +
|
|
"relative plugin path.",
|
|
);
|
|
}
|
|
|
|
val[0] = path.resolve(optionsDir, val[0]);
|
|
} else {
|
|
// check node_modules/babel-x-y
|
|
val[0] = __dirname + "/../../babel-" + type + "-" + val[0];
|
|
}
|
|
|
|
return val;
|
|
});
|
|
}
|
|
|
|
function checkDuplicatedNodes(ast) {
|
|
const nodes = new WeakSet();
|
|
const parents = new WeakMap();
|
|
|
|
const setParent = (child, parent) => {
|
|
if (typeof child === "object" && child !== null) {
|
|
let p = parents.get(child);
|
|
if (!p) {
|
|
p = [];
|
|
parents.set(child, p);
|
|
}
|
|
p.unshift(parent);
|
|
}
|
|
};
|
|
const registerChildren = node => {
|
|
for (const key in node) {
|
|
if (Array.isArray(node[key])) {
|
|
node[key].forEach(child => setParent(child, node));
|
|
} else {
|
|
setParent(node[key], node);
|
|
}
|
|
}
|
|
};
|
|
|
|
const hidePrivateProperties = (key, val) => {
|
|
// Hides properties like _shadowedFunctionLiteral,
|
|
// which makes the AST circular
|
|
if (key[0] === "_") return "[Private]";
|
|
return val;
|
|
};
|
|
|
|
babel.types.traverseFast(ast, node => {
|
|
registerChildren(node);
|
|
if (nodes.has(node)) {
|
|
throw new Error(
|
|
"Do not reuse nodes. Use `t.cloneNode` to copy them.\n" +
|
|
JSON.stringify(node, hidePrivateProperties, 2) +
|
|
"\nParent:\n" +
|
|
JSON.stringify(parents.get(node), hidePrivateProperties, 2),
|
|
);
|
|
}
|
|
nodes.add(node);
|
|
});
|
|
}
|
|
|
|
function run(task) {
|
|
const actual = task.actual;
|
|
const expected = task.expect;
|
|
const exec = task.exec;
|
|
const opts = task.options;
|
|
const optionsDir = task.optionsDir;
|
|
|
|
function getOpts(self) {
|
|
const newOpts = merge(
|
|
{
|
|
cwd: path.dirname(self.filename),
|
|
filename: self.loc,
|
|
filenameRelative: self.filename,
|
|
sourceFileName: self.filename,
|
|
sourceType: "script",
|
|
babelrc: false,
|
|
inputSourceMap: task.inputSourceMap || undefined,
|
|
},
|
|
opts,
|
|
);
|
|
|
|
newOpts.plugins = wrapPackagesArray("plugin", newOpts.plugins, optionsDir);
|
|
newOpts.presets = wrapPackagesArray(
|
|
"preset",
|
|
newOpts.presets,
|
|
optionsDir,
|
|
).map(function(val) {
|
|
if (val.length > 3) {
|
|
throw new Error(
|
|
"Unexpected extra options " +
|
|
JSON.stringify(val.slice(3)) +
|
|
" passed to preset.",
|
|
);
|
|
}
|
|
|
|
return val;
|
|
});
|
|
|
|
return newOpts;
|
|
}
|
|
|
|
let execCode = exec.code;
|
|
let result;
|
|
let resultExec;
|
|
|
|
if (execCode) {
|
|
const execOpts = getOpts(exec);
|
|
result = babel.transform(execCode, execOpts);
|
|
checkDuplicatedNodes(result.ast);
|
|
execCode = result.code;
|
|
|
|
try {
|
|
resultExec = runCodeInTestContext(execCode, execOpts);
|
|
} catch (err) {
|
|
// Pass empty location to include the whole file in the output.
|
|
err.message =
|
|
`${exec.loc}: ${err.message}\n` + codeFrameColumns(execCode, {});
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
let actualCode = actual.code;
|
|
const expectCode = expected.code;
|
|
if (!execCode || actualCode) {
|
|
result = babel.transform(actualCode, getOpts(actual));
|
|
checkDuplicatedNodes(result.ast);
|
|
if (
|
|
!expected.code &&
|
|
result.code &&
|
|
!opts.throws &&
|
|
fs.statSync(path.dirname(expected.loc)).isDirectory() &&
|
|
!process.env.CI
|
|
) {
|
|
const expectedFile = expected.loc.replace(
|
|
/\.m?js$/,
|
|
result.sourceType === "module" ? ".mjs" : ".js",
|
|
);
|
|
|
|
console.log(`New test file created: ${expectedFile}`);
|
|
fs.writeFileSync(expectedFile, `${result.code}\n`);
|
|
|
|
if (expected.loc !== expectedFile) {
|
|
try {
|
|
fs.unlinkSync(expected.loc);
|
|
} catch (e) {}
|
|
}
|
|
} else {
|
|
actualCode = result.code.trim();
|
|
try {
|
|
expect(actualCode).toEqualFile({
|
|
filename: expected.loc,
|
|
code: expectCode,
|
|
});
|
|
} catch (e) {
|
|
if (!process.env.OVERWRITE) throw e;
|
|
|
|
console.log(`Updated test file: ${expected.loc}`);
|
|
fs.writeFileSync(expected.loc, `${result.code}\n`);
|
|
}
|
|
|
|
if (actualCode) {
|
|
expect(expected.loc).toMatch(
|
|
result.sourceType === "module" ? /\.mjs$/ : /\.js$/,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (task.sourceMap) {
|
|
expect(result.map).toEqual(task.sourceMap);
|
|
}
|
|
|
|
if (task.sourceMappings) {
|
|
const consumer = new sourceMap.SourceMapConsumer(result.map);
|
|
|
|
task.sourceMappings.forEach(function(mapping) {
|
|
const actual = mapping.original;
|
|
|
|
const expected = consumer.originalPositionFor(mapping.generated);
|
|
expect({ line: expected.line, column: expected.column }).toEqual(actual);
|
|
});
|
|
}
|
|
|
|
if (execCode && resultExec) {
|
|
return resultExec;
|
|
}
|
|
}
|
|
|
|
const toEqualFile = () => ({
|
|
compare: (actual, { filename, code }) => {
|
|
const pass = actual === code;
|
|
return {
|
|
pass,
|
|
message: () => {
|
|
const diffString = diff(code, actual, {
|
|
expand: false,
|
|
});
|
|
return (
|
|
`Expected ${filename} to match transform output.\n` +
|
|
`To autogenerate a passing version of this file, delete the file and re-run the tests.\n\n` +
|
|
`Diff:\n\n${diffString}`
|
|
);
|
|
},
|
|
};
|
|
},
|
|
negativeCompare: () => {
|
|
throw new Error("Negation unsupported");
|
|
},
|
|
});
|
|
|
|
export default function(
|
|
fixturesLoc: string,
|
|
name: string,
|
|
suiteOpts = {},
|
|
taskOpts = {},
|
|
dynamicOpts?: Function,
|
|
) {
|
|
const suites = getFixtures(fixturesLoc);
|
|
|
|
for (const testSuite of suites) {
|
|
if (includes(suiteOpts.ignoreSuites, testSuite.title)) continue;
|
|
|
|
describe(name + "/" + testSuite.title, function() {
|
|
jest.addMatchers({
|
|
toEqualFile,
|
|
});
|
|
|
|
for (const task of testSuite.tests) {
|
|
if (
|
|
includes(suiteOpts.ignoreTasks, task.title) ||
|
|
includes(suiteOpts.ignoreTasks, testSuite.title + "/" + task.title)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const testFn = task.disabled ? it.skip : it;
|
|
|
|
testFn(
|
|
task.title,
|
|
|
|
function() {
|
|
function runTask() {
|
|
run(task);
|
|
}
|
|
|
|
defaults(task.options, {
|
|
sourceMap: !!(task.sourceMappings || task.sourceMap),
|
|
});
|
|
|
|
extend(task.options, taskOpts);
|
|
|
|
if (dynamicOpts) dynamicOpts(task.options, task);
|
|
|
|
const throwMsg = task.options.throws;
|
|
if (throwMsg) {
|
|
// internal api doesn't have this option but it's best not to pollute
|
|
// the options object with useless options
|
|
delete task.options.throws;
|
|
|
|
assert.throws(runTask, function(err) {
|
|
return throwMsg === true || err.message.indexOf(throwMsg) >= 0;
|
|
});
|
|
} else {
|
|
if (task.exec.code) {
|
|
const result = run(task);
|
|
if (result && typeof result.then === "function") {
|
|
return result;
|
|
}
|
|
} else {
|
|
runTask();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|