UNPKG

8.03 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(
53 `^\n?${tabs ? '\t' : ' '}{${tabs || spaces}}`,
54 'gm'
55 );
56
57 str = str.replace(indentRegex, '').trim();
58 return str;
59}
60
61/**
62 * Create a unified diff between two strings
63 *
64 * @param {Error} err Error object
65 * @param {string} err.actual Actual result returned
66 * @param {string} err.expected Result expected
67 *
68 * @return {string} diff
69 */
70function createUnifiedDiff({ actual, expected }) {
71 return diff
72 .createPatch('string', actual, expected)
73 .split('\n')
74 .splice(4)
75 .map(line => {
76 if (line.match(/@@/)) {
77 return null;
78 }
79 if (line.match(/\\ No newline/)) {
80 return null;
81 }
82 return line.replace(/^(-|\+)/, '$1 ');
83 })
84 .filter(line => typeof line !== 'undefined' && line !== null)
85 .join('\n');
86}
87
88/**
89 * Create an inline diff between two strings
90 *
91 * @param {Error} err Error object
92 * @param {string} err.actual Actual result returned
93 * @param {string} err.expected Result expected
94 *
95 * @return {array} diff string objects
96 */
97function createInlineDiff({ actual, expected }) {
98 return diff.diffWordsWithSpace(actual, expected);
99}
100
101/**
102 * Return a normalized error object
103 *
104 * @param {Error} err Error object
105 *
106 * @return {Object} normalized error
107 */
108function normalizeErr(err, config) {
109 const { name, message, actual, expected, stack, showDiff } = err;
110 let errMessage;
111 let errDiff;
112
113 /**
114 * Check that a / b have the same type.
115 */
116 function sameType(a, b) {
117 const objToString = Object.prototype.toString;
118 return objToString.call(a) === objToString.call(b);
119 }
120
121 // Format actual/expected for creating diff
122 if (
123 showDiff !== false &&
124 sameType(actual, expected) &&
125 expected !== undefined
126 ) {
127 /* istanbul ignore if */
128 if (!(isString(actual) && isString(expected))) {
129 err.actual = mochaUtils.stringify(actual);
130 err.expected = mochaUtils.stringify(expected);
131 }
132 errDiff = config.useInlineDiffs
133 ? createInlineDiff(err)
134 : createUnifiedDiff(err);
135 }
136
137 // Assertion libraries do not output consitent error objects so in order to
138 // get a consistent message object we need to create it ourselves
139 if (name && message) {
140 errMessage = `${name}: ${stripAnsi(message)}`;
141 } else if (stack) {
142 errMessage = stack.replace(/\n.*/g, '');
143 }
144
145 return {
146 message: errMessage,
147 estack: stack && stripAnsi(stack),
148 diff: errDiff,
149 };
150}
151
152/**
153 * Return a plain-object representation of `test`
154 * free of cyclic properties etc.
155 *
156 * @param {Object} test
157 *
158 * @return {Object} cleaned test
159 */
160function cleanTest(test, config) {
161 const code = config.code ? test.body || '' : '';
162
163 const fullTitle = isFunction(test.fullTitle)
164 ? stripAnsi(test.fullTitle())
165 : stripAnsi(test.title);
166
167 const cleaned = {
168 title: stripAnsi(test.title),
169 fullTitle,
170 timedOut: test.timedOut,
171 duration: test.duration || 0,
172 state: test.state,
173 speed: test.speed,
174 pass: test.state === 'passed',
175 fail: test.state === 'failed',
176 pending: test.pending,
177 context: stringify(test.context, null, 2),
178 code: code && cleanCode(code),
179 err: (test.err && normalizeErr(test.err, config)) || {},
180 uuid: test.uuid || /* istanbul ignore next: default */ uuid.v4(),
181 parentUUID: test.parent && test.parent.uuid,
182 isHook: test.type === 'hook',
183 };
184
185 cleaned.skipped =
186 !cleaned.pass && !cleaned.fail && !cleaned.pending && !cleaned.isHook;
187
188 return cleaned;
189}
190
191/**
192 * Return a plain-object representation of `suite` with additional properties for rendering.
193 *
194 * @param {Object} suite
195 * @param {Object} testTotals Cumulative count of tests registered/skipped
196 * @param {Integer} testTotals.registered
197 * @param {Integer} testTotals.skipped
198 *
199 * @return {Object|boolean} cleaned suite or false if suite is empty
200 */
201function cleanSuite(suite, testTotals, config) {
202 let duration = 0;
203 const passingTests = [];
204 const failingTests = [];
205 const pendingTests = [];
206 const skippedTests = [];
207
208 const beforeHooks = []
209 .concat(suite._beforeAll, suite._beforeEach)
210 .map(test => cleanTest(test, config));
211
212 const afterHooks = []
213 .concat(suite._afterAll, suite._afterEach)
214 .map(test => cleanTest(test, config));
215
216 const tests = suite.tests.map(test => {
217 const cleanedTest = cleanTest(test, config);
218 duration += test.duration || 0;
219 if (cleanedTest.state === 'passed') passingTests.push(cleanedTest.uuid);
220 if (cleanedTest.state === 'failed') failingTests.push(cleanedTest.uuid);
221 if (cleanedTest.pending) pendingTests.push(cleanedTest.uuid);
222 if (cleanedTest.skipped) skippedTests.push(cleanedTest.uuid);
223 return cleanedTest;
224 });
225
226 testTotals.registered += tests.length;
227 testTotals.skipped += skippedTests.length;
228
229 const cleaned = {
230 uuid: suite.uuid || /* istanbul ignore next: default */ uuid.v4(),
231 title: stripAnsi(suite.title),
232 fullFile: suite.file || '',
233 file: suite.file ? suite.file.replace(process.cwd(), '') : '',
234 beforeHooks,
235 afterHooks,
236 tests,
237 suites: suite.suites,
238 passes: passingTests,
239 failures: failingTests,
240 pending: pendingTests,
241 skipped: skippedTests,
242 duration,
243 root: suite.root,
244 rootEmpty: suite.root && tests.length === 0,
245 _timeout: suite._timeout,
246 };
247
248 const isEmptySuite =
249 isEmpty(cleaned.suites) &&
250 isEmpty(cleaned.tests) &&
251 isEmpty(cleaned.beforeHooks) &&
252 isEmpty(cleaned.afterHooks);
253
254 return !isEmptySuite && cleaned;
255}
256
257/**
258 * Map over a suite, returning a cleaned suite object
259 * and recursively cleaning any nested suites.
260 *
261 * @param {Object} suite Suite to map over
262 * @param {Object} testTotals Cumulative count of tests registered/skipped
263 * @param {Integer} testTotals.registered
264 * @param {Integer} testTotals.skipped
265 * @param {Object} config Reporter configuration
266 */
267function mapSuites(suite, testTotals, config) {
268 const suites = suite.suites.reduce((acc, subSuite) => {
269 const mappedSuites = mapSuites(subSuite, testTotals, config);
270 if (mappedSuites) {
271 acc.push(mappedSuites);
272 }
273 return acc;
274 }, []);
275 const toBeCleaned = { ...suite, suites };
276 return cleanSuite(toBeCleaned, testTotals, config);
277}
278
279module.exports = {
280 log,
281 cleanCode,
282 cleanTest,
283 cleanSuite,
284 mapSuites,
285};