web-component-tester-istanbulcoverage
Version:
Instanbul coverage reporting for projects being tested by web-component-tester. Web-component-tester version should be greater than 6.4.0
178 lines (152 loc) • 5.28 kB
JavaScript
const _ = require('lodash');
const minimatch = require('minimatch');
const fs = require('fs');
const path = require('path');
const istanbul = require('istanbul');
const scriptHook = require('html-script-hook');
const stripHtmlComments = require('strip-html-comments')
// istanbul
const instrumenter = new istanbul.Instrumenter({
coverageVariable: "WCT.share.__coverage__"
});
// helpers
var cache = {};
const instrumentHtml = (htmlFilePath) => {
var asset = htmlFilePath;
var code;
if ( !cache[asset] ){
let html = fs.readFileSync(htmlFilePath, 'utf8');
html = stripHtmlComments(html);
cache[asset] = scriptHook(html, {scriptCallback: gotScript});
}
return cache[asset];
function gotScript(code, loc) {
return instrumenter.instrumentSync(code, htmlFilePath);
}
};
const instrumentAsset = (assetPath) => {
const asset = assetPath;
let code;
if (!fs.existsSync(assetPath)) {
return;
}
if ( !cache[asset] ) {
code = fs.readFileSync(assetPath, 'utf8');
// NOTE: the instrumenter must get a file system path not a wct-webserver path.
// If given a webserver path it will still generate coverage, but some reporters
// will error, siting that files were not found
// (thedeeno)
cache[asset] = instrumenter.instrumentSync(code, assetPath);
}
return cache[asset];
}
/**
* Returns the instrumented asset depending on if the absolutePath represents an html file or not
* @param {String} absolutePath
*/
const instrumentElem = (absolutePath) => {
if (absolutePath.match(/\.htm(l)?$/)) {
return instrumentHtml(absolutePath);
} else {
return instrumentAsset(absolutePath);
}
};
/**
* Middleware that serves an instrumented asset based on user
* configuration of coverage
*/
const coverageMiddleware = (root, options, wctOpts, emitter) => {
const clientRoot = path.normalize(emitter.options.clientOptions.root);
const pkgName = path.normalize(wctOpts.packageName);
return (req, res, next) => {
let normalizedReqUrl = path.normalize(req.url)
let relativePath = normalizedReqUrl.replace(clientRoot, '');
// we parse the files that where in the component path (pkgName)
if (relativePath.startsWith(pkgName)) {
relativePath = relativePath.replace(path.join(pkgName, path.sep), '');
// check asset against rules
const process = pathMatchesOpt(relativePath, options);
const absolutePath = path.join(root, relativePath);
// instrument unfiltered assets
if ( process ) {
let asset = instrumentElem(absolutePath);
if (asset) {
emitter.emit('log:debug', 'coverage', 'instrument', absolutePath);
return res.send(asset);
}
}
}
emitter.emit('log:debug', 'coverage', 'skip ', relativePath);
return next();
};
};
/**
* Clears the instrumented code cache
*/
const cacheClear = () => {
cache = {};
};
/**
* Returns true if the supplied string mini-matches any of the supplied patterns
*/
const match = (str, rules) => {
return _.some(rules, minimatch.bind(null, str));
};
/**
* Returns the files in the dir parameter directory recursively.
* @param {String} dir: Path where it will read the files recursively.
* @param {List} filelist: It is used in the recursion. First time it can be undefined or empty list.
* @returns A list with all the files in the dir directory.
*/
const readFilesRecursiveSync = (dir, filelist) => {
let files = fs.readdirSync(dir);
filelist = filelist || [];
files.forEach(function(file) {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
filelist = readFilesRecursiveSync(path.join(dir, file), filelist);
}
else {
filelist.push(path.join(dir, file));
}
});
return filelist;
};
/**
* Returns the coveraged of files that have not been processed in the middleware function and that match with the options
* include pattern and they don't match with the options exclude pattern.
* @param {String} dir: Path where we have to search the files with non coverage.
* @param {Object} options:
*/
const getFilesNotCoveraged = (dir, options) => {
const listFiles = readFilesRecursiveSync(dir);
let cacheKeys = Object.keys(cache);
let result = [];
listFiles.forEach(
file => {
const absoluteFile = path.resolve(file);
if (cacheKeys.indexOf(absoluteFile) < 0) {
const matches = pathMatchesOpt(file.replace(path.join(dir, path.sep), ''), options);
if (matches) {
let asset = instrumentElem(absoluteFile);
if (asset) {
const lastCvg = {[absoluteFile]: instrumenter.lastFileCoverage()};
result.push(Object.assign({}, lastCvg));
}
}
}
}
);
return result;
};
function pathMatchesOpt (relativePath, options) {
// always ignore platform files in addition to user's blacklist
let blacklist = ['web-component-tester/*'].concat(options.exclude);
let whitelist = options.include;
// check asset against rules
return match(relativePath, whitelist) && !match(relativePath, blacklist);
}
module.exports = {
middleware: coverageMiddleware,
cacheClear: cacheClear,
getFilesNotCoveraged: getFilesNotCoveraged
};