• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeJoin,
5  ArrayPrototypePop,
6  ArrayPrototypePush,
7  ArrayPrototypeShift,
8  ArrayPrototypeUnshift,
9  hardenRegExp,
10  RegExpPrototypeSymbolSplit,
11  SafeMap,
12  StringPrototypeRepeat,
13} = primordials;
14const assert = require('assert');
15const Transform = require('internal/streams/transform');
16const { inspectWithNoCustomRetry } = require('internal/errors');
17const { green, blue, red, white, gray, shouldColorize } = require('internal/util/colors');
18const { kSubtestsFailed } = require('internal/test_runner/test');
19const { getCoverageReport } = require('internal/test_runner/utils');
20
21const inspectOptions = { __proto__: null, colors: shouldColorize(process.stdout), breakLength: Infinity };
22
23const colors = {
24  '__proto__': null,
25  'test:fail': red,
26  'test:pass': green,
27  'test:diagnostic': blue,
28};
29const symbols = {
30  '__proto__': null,
31  'test:fail': '\u2716 ',
32  'test:pass': '\u2714 ',
33  'test:diagnostic': '\u2139 ',
34  'test:coverage': '\u2139 ',
35  'arrow:right': '\u25B6 ',
36  'hyphen:minus': '\uFE63 ',
37};
38class SpecReporter extends Transform {
39  #stack = [];
40  #reported = [];
41  #indentMemo = new SafeMap();
42  #failedTests = [];
43
44  constructor() {
45    super({ writableObjectMode: true });
46  }
47
48  #indent(nesting) {
49    let value = this.#indentMemo.get(nesting);
50    if (value === undefined) {
51      value = StringPrototypeRepeat('  ', nesting);
52      this.#indentMemo.set(nesting, value);
53    }
54
55    return value;
56  }
57  #formatError(error, indent) {
58    if (!error) return '';
59    const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
60    const message = ArrayPrototypeJoin(
61      RegExpPrototypeSymbolSplit(
62        hardenRegExp(/\r?\n/),
63        inspectWithNoCustomRetry(err, inspectOptions),
64      ), `\n${indent}  `);
65    return `\n${indent}  ${message}\n`;
66  }
67  #formatTestReport(type, data, prefix = '', indent = '', hasChildren = false, skippedSubtest = false) {
68    let color = colors[type] ?? white;
69    let symbol = symbols[type] ?? ' ';
70    const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : '';
71    const title = `${data.name}${duration_ms}${skippedSubtest ? ' # SKIP' : ''}`;
72    if (hasChildren) {
73      // If this test has had children - it was already reported, so slightly modify the output
74      return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n`;
75    }
76    const error = this.#formatError(data.details?.error, indent);
77    if (skippedSubtest) {
78      color = gray;
79      symbol = symbols['hyphen:minus'];
80    }
81    return `${prefix}${indent}${color}${symbol}${title}${white}${error}`;
82  }
83  #handleTestReportEvent(type, data) {
84    const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
85    if (subtest) {
86      assert(subtest.type === 'test:start');
87      assert(subtest.data.nesting === data.nesting);
88      assert(subtest.data.name === data.name);
89    }
90    let prefix = '';
91    while (this.#stack.length) {
92      // Report all the parent `test:start` events
93      const parent = ArrayPrototypePop(this.#stack);
94      assert(parent.type === 'test:start');
95      const msg = parent.data;
96      ArrayPrototypeUnshift(this.#reported, msg);
97      prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
98    }
99    let hasChildren = false;
100    if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
101      ArrayPrototypeShift(this.#reported);
102      hasChildren = true;
103    }
104    const skippedSubtest = subtest && data.skip && data.skip !== undefined;
105    const indent = this.#indent(data.nesting);
106    return `${this.#formatTestReport(type, data, prefix, indent, hasChildren, skippedSubtest)}\n`;
107  }
108  #handleEvent({ type, data }) {
109    switch (type) {
110      case 'test:fail':
111        if (data.details?.error?.failureType !== kSubtestsFailed) {
112          ArrayPrototypePush(this.#failedTests, data);
113        }
114        return this.#handleTestReportEvent(type, data);
115      case 'test:pass':
116        return this.#handleTestReportEvent(type, data);
117      case 'test:start':
118        ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type });
119        break;
120      case 'test:stderr':
121      case 'test:stdout':
122        return data.message;
123      case 'test:diagnostic':
124        return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
125      case 'test:coverage':
126        return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], blue, true);
127    }
128  }
129  _transform({ type, data }, encoding, callback) {
130    callback(null, this.#handleEvent({ type, data }));
131  }
132  _flush(callback) {
133    if (this.#failedTests.length === 0) {
134      callback(null, '');
135      return;
136    }
137    const results = [`\n${colors['test:fail']}${symbols['test:fail']}failing tests:${white}\n`];
138    for (let i = 0; i < this.#failedTests.length; i++) {
139      ArrayPrototypePush(results, this.#formatTestReport(
140        'test:fail',
141        this.#failedTests[i],
142      ));
143    }
144    callback(null, ArrayPrototypeJoin(results, '\n'));
145  }
146}
147
148module.exports = SpecReporter;
149