/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const Base = require('mocha/lib/reporters/base'); const constants = require('mocha/lib/runner').constants; const colors = require('colors/safe'); const milliseconds = require('ms'); const { codeFrameColumns } = require('@babel/code-frame'); const path = require('path'); const fs = require('fs'); const os = require('os'); const terminalLink = require('terminal-link'); const StackUtils = require('stack-utils'); const stackUtils = new StackUtils(); class DotReporter extends Base { constructor(runner, options) { super(runner, options); process.on('SIGINT', async () => { this.epilogue(); process.exit(130); }); runner.on(constants.EVENT_TEST_PENDING, test => { process.stdout.write(colors.yellow('∘')) }); runner.on(constants.EVENT_TEST_PASS, () => { process.stdout.write(colors.green('\u00B7')); }); runner.on(constants.EVENT_TEST_FAIL, test => { if (test.duration >= test.timeout()) process.stdout.write(colors.red('T')); else process.stdout.write(colors.red('F')); }); runner.once(constants.EVENT_RUN_END, () => { process.stdout.write('\n'); this.epilogue(); }); } epilogue() { console.log(''); console.log(colors.green(` ${this.stats.passes || 0} passing`) + colors.dim(` (${milliseconds(this.stats.duration)})`)); if (this.stats.pending) console.log(colors.yellow(` ${this.stats.pending} skipped`)); if (this.stats.failures) { console.log(colors.red(` ${this.stats.failures} failing`)); console.log(''); this.failures.forEach((failure, index) => { const relativePath = path.relative(process.cwd(), failure.file); const header = ` ${index +1}. ${terminalLink(relativePath, `file://${os.hostname()}${failure.file}`)} › ${failure.title}`; console.log(colors.bold.red(header)); const stack = failure.err.stack; if (stack) { console.log(''); const messageLocation = failure.err.stack.indexOf(failure.err.message); const preamble = failure.err.stack.substring(0, messageLocation + failure.err.message.length); console.log(indent(preamble, ' ')); const position = positionInFile(stack, failure.file); if (position) { const source = fs.readFileSync(failure.file, 'utf8'); console.log(''); console.log(indent(codeFrameColumns(source, { start: position, }, { highlightCode: true} ), ' ')); } console.log(''); console.log(indent(colors.dim(stack.substring(preamble.length + 1)), ' ')); } else { console.log(''); console.log(indent(String(failure.err), ' ')); } console.log(''); }); } } } /** * @param {string} lines * @param {string} tab */ function indent(lines, tab) { return lines.replace(/^/gm, tab); } /** * @param {string} stack * @param {string} file * @return {{column: number, line: number}} */ function positionInFile(stack, file) { for (const line of stack.split('\n')) { const parsed = stackUtils.parseLine(line); if (!parsed) continue; if (path.resolve(process.cwd(), parsed.file) === file) return {column: parsed.column, line: parsed.line}; } return null; } module.exports = DotReporter;