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