check-tests
Version:
Static analysis for tests. Prints all tests in console and fails when exclusive or skipped tests found.
273 lines (247 loc) ⢠10.6 kB
JavaScript
require('dotenv').config();
const fs = require('fs');
const Analyzer = require('../src/analyzer');
const Reporter = require('../src/reporter');
const chalk = require('chalk');
const document = require('../src/document');
const { cleanIds, updateIds } = require('../src/updateIds');
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || process.env['TESTOMATIO'];
const branch = process.env.TESTOMATIO_BRANCH;
const debug = require('debug')('testomatio:check');
const { version } = require('../package.json');
console.log(chalk.cyan.bold(` 𤊠Tests checker by Testomat.io v${version}`));
process.env.isTestomatioCli = true;
const program = require('commander');
// Main action function
async function mainAction(framework, files, opts) {
framework = framework.toLowerCase();
opts.testAlias = opts.testAlias ? opts.testAlias.split(',') : [];
opts.framework = framework;
opts.pattern = files;
if (opts.force) {
console.log(' â ď¸ Running with --force option');
}
const frameworkOpts = {};
if (opts.lineNumbers) {
frameworkOpts.lineNumbers = true;
}
if (!opts.hooks) {
frameworkOpts.noHooks = !opts.hooks;
}
const workDir = opts.dir || process.cwd();
const analyzer = new Analyzer(framework, workDir, { ...opts, ...frameworkOpts });
try {
if (opts.typescript) {
try {
require.resolve('typescript');
require.resolve('@typescript-eslint/typescript-estree');
} catch {
console.log('Please install check-tests with TypeScript modules to proceed:');
install(['typescript', '@typescript-eslint/typescript-estree']);
process.exit(1);
}
analyzer.withTypeScript();
}
if (opts.plugins) {
if (!Array.isArray(opts.plugins)) {
opts.plugins = [opts.plugins];
}
opts.plugins.forEach(p => analyzer.addPlugin(p));
}
analyzer.analyze(files);
if (opts.cleanIds || opts.unsafeCleanIds) {
let idMap = {};
if (apiKey) {
const reporter = new Reporter(apiKey.trim(), framework);
idMap = await reporter.getIds();
} else if (opts.cleanIds) {
console.log(' âď¸ API key not provided');
return;
}
const files = cleanIds(analyzer.rawTests, idMap, opts.dir || process.cwd(), {
...opts,
dangerous: opts.unsafeCleanIds,
});
console.log(` ${files.length} files updated.`);
return;
}
const decorator = analyzer.getDecorator();
if (opts.url) {
decorator.fileLink = opts.url;
}
const skipped = decorator.getSkippedTests();
let list = analyzer.getDecorator().getTextList();
list = list.map(l => (l === '-----' ? chalk.bold('_______________________\n') : l)).join('\n');
console.log(chalk.bold.white(`\nSHOWING ${framework.toUpperCase()} TESTS FROM ${files}:`));
console.log(list);
if (skipped.length) {
console.log(chalk.bold.yellow(`\nSKIPPED ${skipped.length} TESTS:\n`));
skipped.forEach(t => console.log(`- ${chalk.bold(t.name)} ${chalk.grey(`${t.file}:${t.line}`)}`));
}
if (decorator.count()) {
console.log(chalk.bold.green(`\n\nTOTAL ${decorator.count()} TESTS FOUND\n`));
if (opts.generateFile) {
console.log(opts.generateFile);
document
.createTestDoc(opts.generateFile, decorator)
.then(() => console.log(`đ Document saved to ${opts.generateFile}`))
.catch(err => console.log('Error in creating test document', err));
}
if (apiKey) {
const reporter = new Reporter(apiKey.trim(), framework);
reporter.addTests(decorator.getTests());
const resp = reporter.send({
sync: opts.sync || opts.updateIds,
create: opts.create || false,
noempty: !opts.empty,
branch,
'no-detach': process.env.TESTOMATIO_NO_DETACHED || !opts.detached,
structure: opts.keepStructure,
force: opts.force || false,
}); // async call
if (opts.sync) {
console.log(' Wait for Testomatio to synchronize tests...');
await resp;
}
if (opts.updateIds) {
if (branch) {
console.log('To avoid conflicts, --update-ids is disabled in a branch. Skipping...');
return;
}
await resp;
console.log(' Updating test ids in the source code...');
analyzer.rawTests = [];
analyzer.analyze(files);
if (apiKey) {
const reporter = new Reporter(apiKey.trim(), framework);
try {
const idMap = await reporter.getIds();
const files = updateIds(analyzer.rawTests, idMap, workDir, opts);
console.log(` ${files.length} files updated.`);
} catch (err) {
console.log(' âď¸ Error in updating test ids', err);
debug(err.message);
}
} else {
console.log(' âď¸ API key not provided');
}
return;
}
} else {
console.log(' âď¸ API key not provided');
}
} else {
console.log(` âď¸ Can't find any tests in this folder: ${workDir}\n`);
console.log(
'Change file pattern or directory to scan to find test files:\n\nUsage: npx check-tests < pattern > -d[directory]',
);
}
if (!opts.skipped && skipped.length) {
throw new Error('Skipped tests found, failing...');
}
} catch (err) {
console.error(chalk.bold.red(err));
console.error(err);
if (!err.name == 'ValidationError') {
console.error(err.stack);
}
process.exit(1);
}
}
program
.arguments('<framework> <files>')
.option('-d, --dir <dir>', 'test directory')
.option('--no-skipped', 'throw error if skipped tests found')
.option('--typescript', 'enable typescript support')
.option('--sync', 'import tests to testomatio and wait for completion')
.option('-g, --generate-file <fileName>', 'Export test details to a document')
.option('-u, --url <url>', 'Github URL to get files (URL/tree/master)')
.option('-p, --plugins [plugins...]', 'additional babel plugins')
.option('--no-detached', 'Don\t mark all unmatched tests as detached')
.option('--update-ids', 'Update test and suite with testomatio ids')
.option('--create', 'Create tests and suites for missing IDs')
.option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io')
.option('--no-empty', 'Remove empty suites after import')
.option('--purge, --unsafe-clean-ids', 'Remove testomatio ids from test and suite without server verification')
.option('--clean-ids', 'Remove testomatio ids from test and suite')
.option('--no-hooks', 'Exclude test hooks code from the code on the client')
.option('--line-numbers', 'Adding an extra line number to each block of code')
.option('--test-alias <test-alias>', 'Specify custom alias for test/it etc (separated by commas if multiple)')
.option('--exclude <pattern>', 'Glob pattern to exclude files from analysis')
.action(mainAction);
// Pull command
program
.command('pull')
.option('-d, --dir <dir>', 'target directory', '.')
.option('--dry-run', 'show what files would be created without actually creating them')
.option('--force', 'skip git checks and force pull files')
.description('Pull test files from Testomat.io')
.action(async opts => {
const Reporter = require('../src/reporter');
const Pull = require('../src/pull');
if (!apiKey) {
console.error(' âď¸ API key not provided. Set TESTOMATIO environment variable.');
process.exit(1);
}
try {
const reporter = new Reporter(apiKey.trim(), 'manual');
const pull = new Pull(reporter, opts.dir || process.cwd(), { dryRun: opts.dryRun, force: opts.force });
const files = await pull.pullFiles();
if (!opts.dryRun && files.length > 0) {
console.log('\n⨠Pull completed successfully!');
}
} catch (error) {
console.error(' âď¸ Failed to pull files:', error.message);
process.exit(1);
}
});
// Push command (alias for check-tests manual **/**.md)
program
.command('push')
.option('-d, --dir <dir>', 'test directory')
.option('--no-skipped', 'throw error if skipped tests found')
.option('--typescript', 'enable typescript support')
.option('--sync', 'import tests to testomatio and wait for completion')
.option('-g, --generate-file <fileName>', 'Export test details to a document')
.option('-u, --url <url>', 'Github URL to get files (URL/tree/master)')
.option('-p, --plugins [plugins...]', 'additional babel plugins')
.option('--no-detached', 'Don\t mark all unmatched tests as detached')
.option('--update-ids', 'Update test and suite with testomatio ids')
.option('--create', 'Create tests and suites for missing IDs')
.option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io')
.option('--no-empty', 'Remove empty suites after import')
.option('--purge, --unsafe-clean-ids', 'Remove testomatio ids from test and suite without server verification')
.option('--clean-ids', 'Remove testomatio ids from test and suite')
.option('--no-hooks', 'Exclude test hooks code from the code on the client')
.option('--line-numbers', 'Adding an extra line number to each block of code')
.option('--test-alias <test-alias>', 'Specify custom alias for test/it etc (separated by commas if multiple)')
.option('--exclude <pattern>', 'Glob pattern to exclude files from analysis')
.option('--force', 'skip git checks and force push files')
.description('Push manual tests from markdown files (alias for check-tests manual **/**.md)')
.action(async opts => {
// Alias: call main action with 'manual' framework and '**/**.md' files
await mainAction('manual', '**/**.md', opts);
});
if (process.argv.length <= 2) {
program.outputHelp();
}
program.parse(process.argv);
async function install(dependencies, verbose) {
return new Promise((resolve, reject) => {
let command;
let args;
dependencies.unshift('check-tests@latest');
console.log('Install required packages locally:');
if (fs.existsSync('yarn.lock')) {
// use yarn
command = 'yarnpkg';
args = ['add', '-D', '--exact'];
[].push.apply(args, dependencies);
} else {
command = 'npm';
args = ['install', '--save-dev'].concat(dependencies);
}
console.log(chalk.green(`${command} ${args.join(' ')}`));
});
}