jest-ex
Version:
A runner and a transformer to simplify (a little bit) your work with Jest.
257 lines (208 loc) • 7.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _path = _interopRequireDefault(require("path"));
var _fs = _interopRequireDefault(require("fs"));
var _glob = _interopRequireDefault(require("glob"));
var _htmlLoader = _interopRequireDefault(require("html-loader"));
var _babel = _interopRequireDefault(require("../babel"));
var _functions = _interopRequireDefault(require("../utils/functions"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* A Jest transfomer that fixes a few issues with Babel and add some 'features' you can use on your
* tests.
*/
class JestExTransformer {
/**
* Class constructor.
*/
constructor() {
/**
* The absolute path to where the script that called the runner was executed.
* @type {String}
*/
this.rootPath = _path.default.resolve(process.cwd());
/**
* A list of lines that I know Babel uses for classes, that are not ignored by Istanbul and
* that may tell you that your coverage is incomplete.
* The reason they are exposed it's because these are the ones I found, but if you find
* others, you can push them to the list.
* @type {Array}
*/
this.invisibleLines = ['return obj && obj.__esModule ? obj : { default: obj };', /((?:var _this(\d+)? =|return) .*?\.__proto__ \|\| \(\d+, _getPrototypeOf.*?\n)/];
/**
* The required comment by Istanbul in order to ignore a line on the code coverage.
* @type {String}
*/
this.ignoreLineComment = 'istanbul ignore next';
/**
* A flag to indicate that `.js(x)` files will be parsed.
* @type {Boolean}
*/
this.processJS = true;
/**
* A flag to indicate that `.html` files will be parsed.
* @type {Boolean}
*/
this.processHTML = true;
/**
* Every time a file is transfomed, this will be its absolute path.
* @type {String}
*/
this.filepath = '';
/**
* Every time a file is transformed, this will be it's source code.
* @type {String}
*/
this.code = '';
/**
* This is binded so it will keep the context even after it gets sent to Jest.
* @ignore
*/
this.process = this.process.bind(this);
}
/**
* This is the method Jest receives and the one that takes care of verifying and transforming
* a file.
* @param {String} src The file source code.
* @param {String} filename The file location.
* @return {String} The transformed code.
*/
process(src, filename) {
let result = src; // Check which kind of file is
const isJS = filename.match(/\.jsx?$/i);
const isHTML = filename.match(/\.html$/i); // Check if the parser is enabled for the known file types
const parseJS = this.processJS && isJS;
const parseHTML = this.processHTML && isHTML; // If the file should be parsed...
if (parseJS || parseHTML) {
this.code = src;
this.filepath = filename;
if (parseJS) {
this._parseJS();
} else {
this._parseHTML();
}
result = this.code;
}
return result;
}
/**
* Parse the file that it's currently being processed as a JS file. This method will first
* format the Jest-Ex special paths, expand the globs, transform it with Babel and finally
* escape the _invisible lines_ for the coverage.
* The changes won't be returned, but made on the current `this.code`.
* @access protected
* @ignore
*/
_parseJS() {
this._formatSpecialPaths(this.filepath);
this._expandUnmockPaths(this.filepath);
this.code = _babel.default.transform(this.code, {
auxiliaryCommentBefore: ` ${this.ignoreLineComment} `,
filename: this.filepath,
presets: [['@babel/preset-env', {
targets: {
node: 'current'
}
}]],
retainLines: true,
plugins: ['@babel/plugin-transform-runtime']
}).code;
this.invisibleLines.forEach(line => this._ignoreLine(line));
}
/**
* Parse the file that it's currently being processed as an HTML file. This method will first
* read it from the file system and then use the Webpack HTML loader to format it as a
* commonjs module.
* The changes won't be returned, but made on the current `this.code`.
* This was done by @sergiolepore on a shared project and I just copied it here :P.
* @access protected
* @ignore
*/
_parseHTML() {
const contents = _fs.default.readFileSync(this.filepath, 'utf-8');
this.code = (0, _htmlLoader.default)(contents);
}
/**
* Given a specific line, it this method will find it on the code of the file that it's
* currently being processed, and it will append the Istanbul comment to ignore it on the code
* coverage.
* The changes won't be returned, but made on the current `this.code`.
* @param {String|RegExp} line The line to _ignore_. It can also be a regular expression.
* @access protected
* @ignore
*/
_ignoreLine(line) {
if (typeof line === 'string') {
const regex = new RegExp(_functions.default.escapeRegex(line), 'g');
this.code = this.code.replace(regex, `/* ${this.ignoreLineComment} */ ${line}`);
} else {
this.code = this.code.replace(line, `/* ${this.ignoreLineComment} */ $1`);
}
}
/**
* Read and transform the custom path format Jest-Ex allows you to use. More information about
* what they are and how to use them in the README.
* The changes won't be returned, but made on the current `this.code`.
* @access protected
* @ignore
*/
_formatSpecialPaths() {
const relative = _path.default.relative(_path.default.dirname(this.filepath), this.rootPath).replace(/\\/g, '/');
[{
regex: /import (.*?) from '(?:\/(.+?)\/)(.*)';/ig,
replacement: `import $1 from '${relative}/$2/$3';`
}, {
regex: /import ({\n(?:[\s\S]*?)}) from '(?:\/(.+?)\/)(.*)';/ig,
replacement: `import $1 from '${relative}/$2/$3';`
}, {
regex: /jest.unmock\('(?:\/(.+?)\/)(.*)'\);/ig,
replacement: `jest.unmock('${relative}/$1/$2');`
}, {
regex: /jest.(setMock|mock)\('(?:\/(.+?)\/)(.*)',/ig,
replacement: `jest.$1('${relative}/$2/$3',`
}, {
regex: /require\('(?:\/(.+?)\/)(.*)'\)/ig,
replacement: `require('${relative}/$1/$2')`
}].forEach(format => {
this.code = this.code.replace(format.regex, format.replacement);
});
}
/**
* Read and transform the `jest.unmock` that use glob patterns instead of file paths.
* The changes won't be returned, but made on the current `this.code`.
* @access protected
* @ignore
*/
_expandUnmockPaths() {
const filedir = _path.default.dirname(this.filepath);
const regex = /jest.unmock\('(.*?\*.*?)'\)/ig;
let match = regex.exec(this.code);
while (match) {
const globRoot = _path.default.dirname(_path.default.relative(this.rootPath, this.filepath));
const [, globPattern] = match;
let globPath = `${globRoot}/${globPattern}`;
let ignoredList = [];
if (globPath.includes('!')) {
const [globPartsPath, globPartsIgnore] = globPath.split('!');
globPath = globPartsPath;
if (globPartsIgnore) {
ignoredList = globPartsIgnore.split(',');
}
}
let lines = '';
_glob.default.sync(globPath).forEach(file => {
if (!ignoredList.filter(value => file.includes(value)).length) {
const fpath = _path.default.relative(filedir, file);
lines += `\njest.unmock('${fpath}');`;
}
});
this.code = this.code.replace(match[0], lines);
match = regex.exec(this.code);
}
}
}
var _default = JestExTransformer;
exports.default = _default;