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