UNPKG

7.84 kBJavaScriptView Raw
1const isString = require('lodash.isstring');
2const isFunction = require('lodash.isfunction');
3const isEmpty = require('lodash.isempty');
4const chalk = require('chalk');
5const uuid = require('uuid');
6const mochaUtils = require('mocha/lib/utils');
7const stringify = require('json-stringify-safe');
8const diff = require('diff');
9const stripAnsi = require('strip-ansi');
10const stripFnStart = require('./stripFnStart');
11
12/**
13 * Return a classname based on percentage
14 *
15 * @param {String} msg - message to log
16 * @param {String} level - log level [log, info, warn, error]
17 * @param {Object} config - configuration object
18 */
19function log(msg, level, config) {
20 // Don't log messages in quiet mode
21 if (config && config.quiet) return;
22 const logMethod = console[level] || console.log;
23 let out = msg;
24 if (typeof msg === 'object') {
25 out = stringify(msg, null, 2);
26 }
27 logMethod(`[${chalk.gray('mochawesome')}] ${out}\n`);
28}
29
30/**
31 * Strip the function definition from `str`,
32 * and re-indent for pre whitespace.
33 *
34 * @param {String} str - code in
35 *
36 * @return {String} cleaned code string
37 */
38function cleanCode(str) {
39 str = str
40 .replace(/\r\n|[\r\n\u2028\u2029]/g, '\n') // unify linebreaks
41 .replace(/^\uFEFF/, ''); // replace zero-width no-break space
42
43 str = stripFnStart(str) // replace function declaration
44 .replace(/\)\s*\)\s*$/, ')') // replace closing paren
45 .replace(/\s*};?\s*$/, ''); // replace closing bracket
46
47 // Preserve indentation by finding leading tabs/spaces
48 // and removing that amount of space from each line
49 const spaces = str.match(/^\n?( *)/)[1].length;
50 const tabs = str.match(/^\n?(\t*)/)[1].length;
51 /* istanbul ignore next */
52 const indentRegex = new RegExp(`^\n?${tabs ? '\t' : ' '}{${tabs || spaces}}`, 'gm');
53
54 str = str.replace(indentRegex, '').trim();
55 return str;
56}
57
58/**
59 * Create a unified diff between two strings
60 *
61 * @param {Error} err Error object
62 * @param {string} err.actual Actual result returned
63 * @param {string} err.expected Result expected
64 *
65 * @return {string} diff
66 */
67function createUnifiedDiff({ actual, expected }) {
68 return diff.createPatch('string', actual, expected)
69 .split('\n')
70 .splice(4)
71 .map(line => {
72 if (line.match(/@@/)) {
73 return null;
74 }
75 if (line.match(/\\ No newline/)) {
76 return null;
77 }
78 return line.replace(/^(-|\+)/, '$1 ');
79 })
80 .filter(line => typeof line !== 'undefined' && line !== null)
81 .join('\n');
82}
83
84/**
85 * Create an inline diff between two strings
86 *
87 * @param {Error} err Error object
88 * @param {string} err.actual Actual result returned
89 * @param {string} err.expected Result expected
90 *
91 * @return {array} diff string objects
92 */
93function createInlineDiff({ actual, expected }) {
94 return diff.diffWordsWithSpace(actual, expected);
95}
96
97/**
98 * Return a normalized error object
99 *
100 * @param {Error} err Error object
101 *
102 * @return {Object} normalized error
103 */
104function normalizeErr(err, config) {
105 const { name, message, actual, expected, stack, showDiff } = err;
106 let errMessage;
107 let errDiff;
108
109 /**
110 * Check that a / b have the same type.
111 */
112 function sameType(a, b) {
113 const objToString = Object.prototype.toString;
114 return objToString.call(a) === objToString.call(b);
115 }
116
117 // Format actual/expected for creating diff
118 if (showDiff !== false && sameType(actual, expected) && expected !== undefined) {
119 /* istanbul ignore if */
120 if (!(isString(actual) && isString(expected))) {
121 err.actual = mochaUtils.stringify(actual);
122 err.expected = mochaUtils.stringify(expected);
123 }
124 errDiff = config.useInlineDiffs ? createInlineDiff(err) : createUnifiedDiff(err);
125 }
126
127 // Assertion libraries do not output consitent error objects so in order to
128 // get a consistent message object we need to create it ourselves
129 if (name && message) {
130 errMessage = `${name}: ${stripAnsi(message)}`;
131 } else if (stack) {
132 errMessage = stack.replace(/\n.*/g, '');
133 }
134
135 return {
136 message: errMessage,
137 estack: stack && stripAnsi(stack),
138 diff: errDiff
139 };
140}
141
142/**
143 * Return a plain-object representation of `test`
144 * free of cyclic properties etc.
145 *
146 * @param {Object} test
147 *
148 * @return {Object} cleaned test
149 */
150function cleanTest(test, config) {
151 const code = config.code ? (test.body || '') : '';
152
153 const fullTitle = isFunction(test.fullTitle)
154 ? stripAnsi(test.fullTitle())
155 : stripAnsi(test.title);
156
157 const cleaned = {
158 title: stripAnsi(test.title),
159 fullTitle,
160 timedOut: test.timedOut,
161 duration: test.duration || 0,
162 state: test.state,
163 speed: test.speed,
164 pass: test.state === 'passed',
165 fail: test.state === 'failed',
166 pending: test.pending,
167 context: stringify(test.context, null, 2),
168 code: code && cleanCode(code),
169 err: (test.err && normalizeErr(test.err, config)) || {},
170 uuid: test.uuid || /* istanbul ignore next: default */uuid.v4(),
171 parentUUID: test.parent && test.parent.uuid,
172 isHook: test.type === 'hook'
173 };
174
175 cleaned.skipped = (!cleaned.pass && !cleaned.fail && !cleaned.pending && !cleaned.isHook);
176
177 return cleaned;
178}
179
180/**
181 * Return a plain-object representation of `suite` with additional properties for rendering.
182 *
183 * @param {Object} suite
184 * @param {Object} totalTestsRegistered
185 * @param {Integer} totalTestsRegistered.total
186 *
187 * @return {Object|boolean} cleaned suite or false if suite is empty
188 */
189function cleanSuite(suite, totalTestsRegistered, config) {
190 let duration = 0;
191 const passingTests = [];
192 const failingTests = [];
193 const pendingTests = [];
194 const skippedTests = [];
195
196 const beforeHooks = [].concat(
197 suite._beforeAll, suite._beforeEach
198 ).map(test => cleanTest(test, config));
199
200 const afterHooks = [].concat(
201 suite._afterAll, suite._afterEach
202 ).map(test => cleanTest(test, config));
203
204 const tests = suite.tests.map(test => {
205 const cleanedTest = cleanTest(test, config);
206 duration += test.duration || 0;
207 if (cleanedTest.state === 'passed') passingTests.push(cleanedTest.uuid);
208 if (cleanedTest.state === 'failed') failingTests.push(cleanedTest.uuid);
209 if (cleanedTest.pending) pendingTests.push(cleanedTest.uuid);
210 if (cleanedTest.skipped) skippedTests.push(cleanedTest.uuid);
211 return cleanedTest;
212 });
213
214 totalTestsRegistered.total += tests.length;
215
216 const cleaned = {
217 uuid: suite.uuid || /* istanbul ignore next: default */uuid.v4(),
218 title: stripAnsi(suite.title),
219 fullFile: suite.file || '',
220 file: suite.file ? suite.file.replace(process.cwd(), '') : '',
221 beforeHooks,
222 afterHooks,
223 tests,
224 suites: suite.suites,
225 passes: passingTests,
226 failures: failingTests,
227 pending: pendingTests,
228 skipped: skippedTests,
229 duration,
230 root: suite.root,
231 rootEmpty: suite.root && tests.length === 0,
232 _timeout: suite._timeout
233 };
234
235 const isEmptySuite = isEmpty(cleaned.suites)
236 && isEmpty(cleaned.tests)
237 && isEmpty(cleaned.beforeHooks)
238 && isEmpty(cleaned.afterHooks);
239
240 return !isEmptySuite && cleaned;
241}
242
243/**
244 * Map over a suite, returning a cleaned suite object
245 * and recursively cleaning any nested suites.
246 *
247 * @param {Object} suite Suite to map over
248 * @param {Object} totalTestsReg Cumulative count of total tests registered
249 * @param {Integer} totalTestsReg.total
250 * @param {Object} config Reporter configuration
251 */
252function mapSuites(suite, totalTestsReg, config) {
253 const suites = suite.suites.reduce((acc, subSuite) => {
254 const mappedSuites = mapSuites(subSuite, totalTestsReg, config);
255 if (mappedSuites) {
256 acc.push(mappedSuites);
257 }
258 return acc;
259 }, []);
260 const toBeCleaned = { ...suite, suites };
261 return cleanSuite(toBeCleaned, totalTestsReg, config);
262}
263
264module.exports = {
265 log,
266 cleanCode,
267 cleanTest,
268 cleanSuite,
269 mapSuites
270};