UNPKG

mocha

Version:

Classic, reliable, trusted test framework for Node.js and the browser

151 lines (134 loc) 3.82 kB
/** * @typedef {import('../runner.cjs')} Runner * @typedef {import('../test.js')} Test */ /** * @module GithubActions */ /** * Module dependencies. */ import fs from "node:fs"; import ms from "ms"; import { Spec } from "./spec.js"; import Runner from "../runner.cjs"; const { constants } = Runner; var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; var EVENT_RUN_END = constants.EVENT_RUN_END; /** * Extract file, line, and column from an error stack trace. * * @private * @param {string} [stackTrace] - stack trace string * @return {{ file?: string, line?: string, col?: string }} */ function extractLocation(stackTrace) { if (!stackTrace) { return {}; } var matches = /^\s*at Context.*\(([^()]+):([0-9]+):([0-9]+)\)/gm.exec(stackTrace) || /^\s*at.*\(([^()]+):([0-9]+):([0-9]+)\)/gm.exec(stackTrace); if (matches === null) { return {}; } return { file: matches[1], line: matches[2], col: matches[3], }; } /** * Escape a value for use in a GitHub Actions workflow command. * * @private * @param {string} value * @return {string} */ function escapeWorkflowValue(value) { return String(value) .replace(/%/g, "%25") .replace(/\r/g, "%0D") .replace(/\n/g, "%0A"); } /** * Escape a property value for use in a GitHub Actions workflow command. * * @private * @param {string} value * @return {string} */ function escapeWorkflowProperty(value) { return String(value) .replace(/%/g, "%25") .replace(/\r/g, "%0D") .replace(/\n/g, "%0A") .replace(/:/g, "%3A") .replace(/,/g, "%2C"); } class GithubActions extends Spec { static description = "like spec, with GitHub Actions error annotations and job summary"; /** * Construct a new GithubActions reporter instance. * * @description * Extend the Spec reporter to add GitHub Actions error annotations and * Job Summary output when running in GitHub Actions CI. * * @public * @memberof Mocha.reporters * @extends Mocha.reporters.Spec * @param {Runner} runner - Instance triggers reporter actions. * @param {Object} [options] - runner options */ constructor(runner, options) { super(runner, options); var ciFailures = []; runner.on(EVENT_TEST_FAIL, function (test) { ciFailures.push(test); }); runner.once(EVENT_RUN_END, () => { if (ciFailures.length > 0) { process.stdout.write("::group::Mocha Annotations\n"); for (var test of ciFailures) { var location = extractLocation(test.err && test.err.stack); var props = []; if (location.file) { props.push("file=" + escapeWorkflowProperty(location.file)); } if (location.line) { props.push("line=" + escapeWorkflowProperty(location.line)); } if (location.col) { props.push("col=" + escapeWorkflowProperty(location.col)); } var message = test.err ? escapeWorkflowValue(test.err.message) : ""; process.stdout.write( "::error " + props.join(",") + "::" + message + "\n", ); } process.stdout.write("::endgroup::\n"); } var summaryFile = process.env.GITHUB_STEP_SUMMARY; if (summaryFile) { var lines = []; lines.push( ":white_check_mark: " + (this.stats.passes || 0) + " passing (" + ms(this.stats.duration) + ")", ); if (this.stats.pending) { lines.push(":pause_button: " + this.stats.pending + " pending"); } if (this.stats.failures) { lines.push(":x: " + this.stats.failures + " failing"); } fs.appendFileSync(summaryFile, lines.join("\n") + "\n"); } }); } } export { GithubActions };