UNPKG

8 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 let code = test.body;
152
153 /* istanbul ignore next */
154 if (code === undefined) {
155 /* istanbul ignore next: test.fn exists prior to mocha 2.4.0 */
156 code = test.fn ? test.fn.toString() : '';
157 }
158
159 const cleaned = {
160 title: stripAnsi(test.title),
161 fullTitle: isFunction(test.fullTitle) ? stripAnsi(test.fullTitle()) : /* istanbul ignore next */ stripAnsi(test.title),
162 timedOut: test.timedOut,
163 duration: test.duration || 0,
164 state: test.state,
165 speed: test.speed,
166 pass: test.state === 'passed',
167 fail: test.state === 'failed',
168 pending: test.pending,
169 context: stringify(test.context, null, 2),
170 code: code && cleanCode(code),
171 err: (test.err && normalizeErr(test.err, config)) || {},
172 uuid: test.uuid || /* istanbul ignore next: default */uuid.v4(),
173 parentUUID: test.parent && test.parent.uuid,
174 isHook: test.type === 'hook'
175 };
176
177 cleaned.skipped = (!cleaned.pass && !cleaned.fail && !cleaned.pending && !cleaned.isHook);
178
179 return cleaned;
180}
181
182/**
183 * Return a plain-object representation of `suite` with additional properties for rendering.
184 *
185 * @param {Object} suite
186 * @param {Object} totalTestsRegistered
187 * @param {Integer} totalTestsRegistered.total
188 *
189 * @return {Object|boolean} cleaned suite or false if suite is empty
190 */
191function cleanSuite(suite, totalTestsRegistered, config) {
192 let duration = 0;
193 const passingTests = [];
194 const failingTests = [];
195 const pendingTests = [];
196 const skippedTests = [];
197
198 const beforeHooks = [].concat(
199 suite._beforeAll, suite._beforeEach
200 ).map(test => cleanTest(test, config));
201
202 const afterHooks = [].concat(
203 suite._afterAll, suite._afterEach
204 ).map(test => cleanTest(test, config));
205
206 const tests = suite.tests.map(test => {
207 const cleanedTest = cleanTest(test, config);
208 duration += test.duration || 0;
209 if (cleanedTest.state === 'passed') passingTests.push(cleanedTest.uuid);
210 if (cleanedTest.state === 'failed') failingTests.push(cleanedTest.uuid);
211 if (cleanedTest.pending) pendingTests.push(cleanedTest.uuid);
212 if (cleanedTest.skipped) skippedTests.push(cleanedTest.uuid);
213 return cleanedTest;
214 });
215
216 totalTestsRegistered.total += tests.length;
217
218 const cleaned = {
219 uuid: suite.uuid || /* istanbul ignore next: default */uuid.v4(),
220 title: stripAnsi(suite.title),
221 fullFile: suite.file || '',
222 file: suite.file ? suite.file.replace(process.cwd(), '') : '',
223 beforeHooks,
224 afterHooks,
225 tests,
226 suites: suite.suites,
227 passes: passingTests,
228 failures: failingTests,
229 pending: pendingTests,
230 skipped: skippedTests,
231 duration,
232 root: suite.root,
233 rootEmpty: suite.root && tests.length === 0,
234 _timeout: suite._timeout
235 };
236
237 const isEmptySuite = isEmpty(cleaned.suites)
238 && isEmpty(cleaned.tests)
239 && isEmpty(cleaned.beforeHooks)
240 && isEmpty(cleaned.afterHooks);
241
242 return !isEmptySuite && cleaned;
243}
244
245/**
246 * Map over a suite, returning a cleaned suite object
247 * and recursively cleaning any nested suites.
248 *
249 * @param {Object} suite Suite to map over
250 * @param {Object} totalTestsReg Cumulative count of total tests registered
251 * @param {Integer} totalTestsReg.total
252 * @param {Object} config Reporter configuration
253 */
254function mapSuites(suite, totalTestsReg, config) {
255 const suites = suite.suites.reduce((acc, subSuite) => {
256 const mappedSuites = mapSuites(subSuite, totalTestsReg, config);
257 if (mappedSuites) {
258 acc.push(mappedSuites);
259 }
260 return acc;
261 }, []);
262 const toBeCleaned = Object.assign({}, suite, { suites });
263 return cleanSuite(toBeCleaned, totalTestsReg, config);
264}
265
266module.exports = {
267 log,
268 cleanCode,
269 cleanTest,
270 cleanSuite,
271 mapSuites
272};