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