• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// @ts-check
2const Mocha = require("mocha");
3const path = require("path");
4const fs = require("fs");
5const os = require("os");
6
7const failingHookRegExp = /^(.*) "(before|after) (all|each)" hook$/;
8
9/**
10 * .failed-tests reporter
11 *
12 * @typedef {Object} ReporterOptions
13 * @property {string} [file]
14 * @property {boolean} [keepFailed]
15 * @property {string|Mocha.ReporterConstructor} [reporter]
16 * @property {*} [reporterOptions]
17 */
18class FailedTestsReporter extends Mocha.reporters.Base {
19    /**
20     * @param {Mocha.Runner} runner
21     * @param {{ reporterOptions?: ReporterOptions }} [options]
22     */
23    constructor(runner, options) {
24        super(runner, options);
25        if (!runner) return;
26
27        const reporterOptions = this.reporterOptions = options.reporterOptions || {};
28        if (reporterOptions.file === undefined) reporterOptions.file = ".failed-tests";
29        if (reporterOptions.keepFailed === undefined) reporterOptions.keepFailed = false;
30        if (reporterOptions.reporter) {
31            /** @type {Mocha.ReporterConstructor} */
32            let reporter;
33            if (typeof reporterOptions.reporter === "function") {
34                reporter = reporterOptions.reporter;
35            }
36            else if (Mocha.reporters[reporterOptions.reporter]) {
37                reporter = Mocha.reporters[reporterOptions.reporter];
38            }
39            else {
40                try {
41                    reporter = require(reporterOptions.reporter);
42                }
43                catch (_) {
44                    reporter = require(path.resolve(process.cwd(), reporterOptions.reporter));
45                }
46            }
47
48            const newOptions = Object.assign({}, options, { reporterOptions: reporterOptions.reporterOptions || {} });
49            if (reporterOptions.reporter === "xunit") {
50                newOptions.reporterOptions.output = "TEST-results.xml";
51            }
52            this.reporter = new reporter(runner, newOptions);
53        }
54
55        /** @type {Mocha.Test[]} */
56        this.passes = [];
57
58        /** @type {(Mocha.Test)[]} */
59        this.failures = [];
60
61        runner.on("pass", test => this.passes.push(test));
62        runner.on("fail", test => this.failures.push(test));
63    }
64
65    /**
66     * @param {string} file
67     * @param {ReadonlyArray<Mocha.Test>} passes
68     * @param {ReadonlyArray<Mocha.Test | Mocha.Hook>} failures
69     * @param {boolean} keepFailed
70     * @param {(err?: NodeJS.ErrnoException) => void} done
71     */
72    static writeFailures(file, passes, failures, keepFailed, done) {
73        const failingTests = new Set(fs.existsSync(file) ? readTests() : undefined);
74        const possiblyPassingSuites = /**@type {Set<string>}*/(new Set());
75
76        // Remove tests that are now passing and track suites that are now
77        // possibly passing.
78        if (failingTests.size > 0 && !keepFailed) {
79            for (const test of passes) {
80                failingTests.delete(test.fullTitle().trim());
81                possiblyPassingSuites.add(test.parent.fullTitle().trim());
82            }
83        }
84
85        // Add tests that are now failing. If a hook failed, track its
86        // containing suite as failing. If the suite for a test or hook was
87        // possibly passing then it is now definitely failing.
88        for (const test of failures) {
89            const suiteTitle = test.parent.fullTitle().trim();
90            if (test.type === "test") {
91                failingTests.add(test.fullTitle().trim());
92            }
93            else {
94                failingTests.add(suiteTitle);
95            }
96            possiblyPassingSuites.delete(suiteTitle);
97        }
98
99        // Remove all definitely passing suites.
100        for (const suite of possiblyPassingSuites) {
101            failingTests.delete(suite);
102        }
103
104        if (failingTests.size > 0) {
105            const failed = Array
106                .from(failingTests)
107                .sort()
108                .join(os.EOL);
109            fs.writeFile(file, failed, "utf8", done);
110        }
111        else if (!keepFailed && fs.existsSync(file)) {
112            fs.unlink(file, done);
113        }
114        else {
115            done();
116        }
117
118        function readTests() {
119            return fs.readFileSync(file, "utf8")
120                .split(/\r?\n/g)
121                .map(line => line.trim())
122                .filter(line => line.length > 0);
123        }
124    }
125
126    /**
127     * @param {number} failures
128     * @param {(failures: number) => void} [fn]
129     */
130    done(failures, fn) {
131        FailedTestsReporter.writeFailures(this.reporterOptions.file, this.passes, this.failures, this.reporterOptions.keepFailed || this.stats.tests === 0, (err) => {
132            const reporter = this.reporter;
133            if (reporter && reporter.done) {
134                reporter.done(failures, fn);
135            }
136            else if (fn) {
137                fn(failures);
138            }
139
140            if (err) console.error(err);
141        });
142    }
143}
144
145module.exports = FailedTestsReporter;