@testomatio/reporter 1.6.0-beta-2-artifacts → 2.0.0-beta-esm

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/lib/adapter/codecept.js +288 -330
  2. package/lib/adapter/cucumber/current.js +195 -203
  3. package/lib/adapter/cucumber/legacy.js +130 -155
  4. package/lib/adapter/cucumber.js +5 -16
  5. package/lib/adapter/cypress-plugin/index.js +91 -105
  6. package/lib/adapter/jasmine/jasmine.js +63 -0
  7. package/lib/adapter/jasmine.js +54 -53
  8. package/lib/adapter/jest.js +97 -99
  9. package/lib/adapter/mocha/mocha.js +125 -0
  10. package/lib/adapter/mocha.js +111 -140
  11. package/lib/adapter/playwright.js +168 -200
  12. package/lib/adapter/vitest.js +144 -143
  13. package/lib/adapter/webdriver.js +113 -97
  14. package/lib/bin/reportXml.js +49 -49
  15. package/lib/bin/startTest.js +80 -97
  16. package/lib/client.js +344 -385
  17. package/lib/config.js +16 -21
  18. package/lib/constants.js +49 -43
  19. package/lib/data-storage.js +206 -188
  20. package/lib/fileUploader.js +245 -0
  21. package/lib/junit-adapter/adapter.js +17 -20
  22. package/lib/junit-adapter/csharp.js +18 -14
  23. package/lib/junit-adapter/index.js +27 -25
  24. package/lib/junit-adapter/java.js +41 -53
  25. package/lib/junit-adapter/javascript.js +30 -27
  26. package/lib/junit-adapter/python.js +38 -37
  27. package/lib/junit-adapter/ruby.js +11 -8
  28. package/lib/output.js +44 -52
  29. package/lib/package.json +1 -0
  30. package/lib/pipe/bitbucket.js +208 -227
  31. package/lib/pipe/csv.js +111 -124
  32. package/lib/pipe/github.js +184 -211
  33. package/lib/pipe/gitlab.js +164 -205
  34. package/lib/pipe/html.js +253 -312
  35. package/lib/pipe/index.js +83 -63
  36. package/lib/pipe/testomatio.js +391 -454
  37. package/lib/reporter-functions.js +16 -20
  38. package/lib/reporter.js +47 -17
  39. package/lib/services/artifacts.js +55 -51
  40. package/lib/services/index.js +14 -12
  41. package/lib/services/key-values.js +56 -53
  42. package/lib/services/logger.js +227 -245
  43. package/lib/utils/chalk.js +10 -0
  44. package/lib/utils/pipe_utils.js +91 -84
  45. package/lib/utils/utils.js +289 -273
  46. package/lib/xmlReader.js +480 -519
  47. package/package.json +57 -19
  48. package/src/adapter/codecept.js +369 -0
  49. package/src/adapter/cucumber/current.js +228 -0
  50. package/src/adapter/cucumber/legacy.js +158 -0
  51. package/src/adapter/cucumber.js +4 -0
  52. package/src/adapter/cypress-plugin/index.js +110 -0
  53. package/src/adapter/jasmine.js +60 -0
  54. package/src/adapter/jest.js +107 -0
  55. package/src/adapter/mocha.cjs +2 -0
  56. package/src/adapter/mocha.js +156 -0
  57. package/src/adapter/playwright.js +222 -0
  58. package/src/adapter/vitest.js +183 -0
  59. package/src/adapter/webdriver.js +111 -0
  60. package/src/bin/reportXml.js +67 -0
  61. package/src/bin/startTest.js +119 -0
  62. package/src/client.js +423 -0
  63. package/src/config.js +30 -0
  64. package/src/constants.js +49 -0
  65. package/src/data-storage.js +204 -0
  66. package/src/fileUploader.js +307 -0
  67. package/src/junit-adapter/adapter.js +23 -0
  68. package/src/junit-adapter/csharp.js +16 -0
  69. package/src/junit-adapter/index.js +28 -0
  70. package/src/junit-adapter/java.js +58 -0
  71. package/src/junit-adapter/javascript.js +31 -0
  72. package/src/junit-adapter/python.js +42 -0
  73. package/src/junit-adapter/ruby.js +10 -0
  74. package/src/output.js +57 -0
  75. package/src/pipe/bitbucket.js +254 -0
  76. package/src/pipe/csv.js +140 -0
  77. package/src/pipe/github.js +234 -0
  78. package/src/pipe/gitlab.js +229 -0
  79. package/src/pipe/html.js +366 -0
  80. package/src/pipe/index.js +73 -0
  81. package/src/pipe/testomatio.js +498 -0
  82. package/src/reporter-functions.js +44 -0
  83. package/src/reporter.cjs +22 -0
  84. package/src/reporter.js +24 -0
  85. package/src/services/artifacts.js +59 -0
  86. package/src/services/index.js +13 -0
  87. package/src/services/key-values.js +59 -0
  88. package/src/services/logger.js +314 -0
  89. package/src/template/emptyData.svg +23 -0
  90. package/src/template/testomatio.hbs +1421 -0
  91. package/src/utils/chalk.js +13 -0
  92. package/src/utils/pipe_utils.js +127 -0
  93. package/src/utils/utils.js +341 -0
  94. package/src/xmlReader.js +551 -0
  95. package/lib/bin/cli.js +0 -216
  96. package/lib/bin/uploadArtifacts.js +0 -86
  97. package/lib/uploader.js +0 -312
@@ -0,0 +1,13 @@
1
+ // import chalk from 'chalk';
2
+
3
+ // let chalkToUse = chalk;
4
+
5
+ // try {
6
+ // if (typeof require !== 'undefined') {
7
+ // chalkToUse = require('chalk4');
8
+ // }
9
+ // } catch (e) {
10
+ // chalkToUse = chalk;
11
+ // }
12
+
13
+ // export default chalkToUse;
@@ -0,0 +1,127 @@
1
+ import { upload } from '../fileUploader.js';
2
+ import { APP_PREFIX } from '../constants.js';
3
+
4
+ /**
5
+ * Set S3 credentials from the provided artifacts object.
6
+ * @param {Object} artifacts - The artifacts object containing S3 credentials.
7
+ */
8
+ function setS3Credentials(artifacts) {
9
+ if (!Object.keys(artifacts).length) return;
10
+
11
+ console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
12
+
13
+ if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
14
+ if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
15
+ if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
16
+ if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
17
+ if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
18
+ if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
19
+ if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
20
+ upload.resetConfig();
21
+ }
22
+
23
+ /**
24
+ * Generates mode request parameters based on the input params.
25
+ * @param {{type: string, id?: string, apiKey: string}} params - The input parameters for the request.
26
+ * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
27
+ */
28
+ function generateFilterRequestParams(params) {
29
+ const { type, id, apiKey } = params;
30
+
31
+ if (!type) {
32
+ return;
33
+ }
34
+
35
+ if (!id) {
36
+ console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
37
+ return;
38
+ }
39
+
40
+ return {
41
+ params: {
42
+ type,
43
+ id: encodeURIComponent(id),
44
+ api_key: apiKey,
45
+ },
46
+ responseType: 'json',
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Parse filter parameters from a string in the format "type=id".
52
+ * @param {string} opts - The input string containing the filter parameters.
53
+ * @returns {Object} An object containing the parsed filter parameters.
54
+ * The object has properties "type" and "id".
55
+ */
56
+ function parseFilterParams(opts) {
57
+ const [type, id] = opts.split('=');
58
+ const validType = updateFilterType(type);
59
+
60
+ return {
61
+ type: validType,
62
+ id,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Update and validate the filter type.
68
+ * @param {string} type - The original filter type.
69
+ * @returns {string|undefined} The updated and validated filter type.
70
+ * Returns undefined if the type is not valid.
71
+ */
72
+ function updateFilterType(type) {
73
+ const typeLowerCase = type.toLowerCase();
74
+
75
+ const filterTypes = ['tag-name', 'plan-id', 'label', 'jira-ticket'];
76
+
77
+ const filterApi = [
78
+ 'tag',
79
+ 'plan',
80
+ 'label',
81
+ 'jira',
82
+ // "ims-issue", //TODO: WIP
83
+ ];
84
+
85
+ if (!filterTypes.includes(typeLowerCase)) {
86
+ console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
87
+ return;
88
+ }
89
+
90
+ const index = filterTypes.indexOf(typeLowerCase);
91
+
92
+ return index !== -1 ? filterApi[index] : undefined;
93
+ }
94
+
95
+ /**
96
+ * Return an emoji based on the provided status.
97
+ * @param {string} status - The status value ('passed', 'failed', or 'skipped').
98
+ * @returns {string} - An emoji corresponding to the provided status.
99
+ */
100
+ function statusEmoji(status) {
101
+ if (status === 'passed') return '🟢';
102
+ if (status === 'failed') return '🔴';
103
+ if (status === 'skipped') return '🟡';
104
+ return '';
105
+ }
106
+
107
+ /**
108
+ * Generate a full name string based on the provided test object.
109
+ * @param {object} t - The test object.
110
+ * @returns {string} - A formatted full name string for the test object.
111
+ */
112
+ function fullName(t) {
113
+ let line = '';
114
+ if (t.suite_title) line = `${t.suite_title}: `;
115
+ line += `**${t.title}**`;
116
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
117
+ return line;
118
+ }
119
+
120
+ export {
121
+ updateFilterType,
122
+ parseFilterParams,
123
+ generateFilterRequestParams,
124
+ setS3Credentials,
125
+ statusEmoji,
126
+ fullName,
127
+ };
@@ -0,0 +1,341 @@
1
+ import { URL } from 'url';
2
+ import { sep, basename } from 'path';
3
+ import pc from 'picocolors';
4
+ import fs from 'fs';
5
+ import isValid from 'is-valid-path';
6
+ import createDebugMessages from 'debug';
7
+
8
+ const debug = createDebugMessages('@testomatio/reporter:util');
9
+
10
+ /**
11
+ * @param {String} testTitle - Test title
12
+ *
13
+ * @returns {String|null} testId
14
+ */
15
+ const getTestomatIdFromTestTitle = testTitle => {
16
+ if (!testTitle) return null;
17
+
18
+ const captures = testTitle.match(/@T[\w\d]{8}/);
19
+
20
+ if (captures) {
21
+ return captures[0];
22
+ }
23
+
24
+ return null;
25
+ };
26
+
27
+ /**
28
+ * @param {String} suiteTitle - suite title
29
+ *
30
+ * @returns {String|null} suiteId
31
+ */
32
+ const parseSuite = suiteTitle => {
33
+ const captures = suiteTitle.match(/@S[\w\d]{8}/);
34
+ if (captures) {
35
+ return captures[1];
36
+ }
37
+
38
+ return null;
39
+ };
40
+
41
+ const ansiRegExp = () => {
42
+ const pattern = [
43
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
44
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
45
+ ].join('|');
46
+
47
+ return new RegExp(pattern, 'g');
48
+ };
49
+
50
+ const isValidUrl = s => {
51
+ try {
52
+ // eslint-disable-next-line no-new
53
+ new URL(s);
54
+ return true;
55
+ } catch (err) {
56
+ return false;
57
+ }
58
+ };
59
+
60
+ const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/gi;
61
+
62
+ const fetchFilesFromStackTrace = (stack = '') => {
63
+ const files = Array.from(stack.matchAll(fileMatchRegex))
64
+ .map(f => f[1].trim())
65
+ .map(f => (f.startsWith('//') ? f.substring(1) : f));
66
+
67
+ debug('Found files in stack trace: ', files);
68
+
69
+ return files.filter(f => {
70
+ const isFile = fs.existsSync(f);
71
+ if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
72
+ return isFile;
73
+ });
74
+ };
75
+
76
+ const fetchSourceCodeFromStackTrace = (stack = '') => {
77
+ const stackLines = stack
78
+ .split('\n')
79
+ .filter(l => l.includes(':'))
80
+ // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
81
+ // .map(l => l.split(':')[0])
82
+ .map(l => l.trim())
83
+ .map(l => l.split(' ').find(p => p.includes(':')) || '')
84
+ .filter(l => isValid(l?.split(':')[0]))
85
+
86
+ // // filter out 3rd party libs
87
+ .filter(l => !l?.includes(`vendor${sep}`))
88
+ .filter(l => !l?.includes(`node_modules${sep}`))
89
+ .filter(l => fs.existsSync(l.split(':')[0]))
90
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile());
91
+
92
+ if (!stackLines.length) return '';
93
+
94
+ const [file, line] = stackLines[0].split(':');
95
+
96
+ const prepend = 3;
97
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
98
+
99
+ if (!source) return '';
100
+
101
+ return source
102
+ .split('\n')
103
+ .map((l, i) => {
104
+ if (i === prepend) return `${line} > ${pc.bold(l)}`;
105
+ return `${+line - prepend + i} | ${l}`;
106
+ })
107
+ .join('\n');
108
+ };
109
+
110
+ const TEST_ID_REGEX = /@T([\w\d]{8})/;
111
+
112
+ const fetchIdFromCode = (code, opts = {}) => {
113
+ const comments = code
114
+ .split('\n')
115
+ .map(l => l.trim())
116
+ .filter(l => {
117
+ switch (opts.lang) {
118
+ case 'ruby':
119
+ case 'python':
120
+ return l.startsWith('# ');
121
+ default:
122
+ return l.startsWith('// ');
123
+ }
124
+ });
125
+
126
+ return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
127
+ };
128
+
129
+ const fetchIdFromOutput = output => {
130
+ const lines = output
131
+ .split('\n')
132
+ .map(l => l.trim())
133
+ .filter(l => l.startsWith('tid://'));
134
+
135
+ return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
136
+ };
137
+
138
+ const fetchSourceCode = (contents, opts = {}) => {
139
+ if (!opts.title && !opts.line) return '';
140
+
141
+ // code fragment is 20 lines
142
+ const limit = opts.limit || 50;
143
+ let lineIndex;
144
+ if (opts.line) lineIndex = opts.line - 1;
145
+ const lines = contents.split('\n');
146
+
147
+ // remove special chars from title
148
+ if (!lineIndex && opts.title) {
149
+ const title = opts.title.replace(/[([@].*/g, '');
150
+
151
+ if (opts.lang === 'java') {
152
+ lineIndex = lines.findIndex(l => l.includes(`test${title}`));
153
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
154
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
155
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
156
+ } else {
157
+ lineIndex = lines.findIndex(l => l.includes(title));
158
+ }
159
+ }
160
+
161
+ if (opts.prepend) {
162
+ lineIndex -= opts.prepend;
163
+ }
164
+
165
+ if (lineIndex) {
166
+ const result = [];
167
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
168
+ if (lines[i] === undefined) continue;
169
+
170
+ if (i > lineIndex + 2 && !opts.prepend) {
171
+ // annotation
172
+ if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
173
+ if (opts.lang === 'php' && lines[i].includes(' private function ')) break;
174
+ if (opts.lang === 'php' && lines[i].includes(' protected function ')) break;
175
+ if (opts.lang === 'php' && lines[i].includes(' public function ')) break;
176
+ if (opts.lang === 'python' && lines[i].trim().match(/^@\w+/)) break;
177
+ if (opts.lang === 'python' && lines[i].includes(' def ')) break;
178
+ if (opts.lang === 'ruby' && lines[i].includes(' def ')) break;
179
+ if (opts.lang === 'ruby' && lines[i].includes(' test ')) break;
180
+ if (opts.lang === 'ruby' && lines[i].includes(' it ')) break;
181
+ if (opts.lang === 'ruby' && lines[i].includes(' specify ')) break;
182
+ if (opts.lang === 'ruby' && lines[i].includes(' context ')) break;
183
+ if (opts.lang === 'ts' && lines[i].includes(' it(')) break;
184
+ if (opts.lang === 'ts' && lines[i].includes(' test(')) break;
185
+ if (opts.lang === 'js' && lines[i].includes(' it(')) break;
186
+ if (opts.lang === 'js' && lines[i].includes(' test(')) break;
187
+ if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
188
+ if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
189
+ if (opts.lang === 'java' && lines[i].includes(' class ')) break;
190
+ }
191
+ result.push(lines[i]);
192
+ }
193
+ return result.join('\n');
194
+ }
195
+ };
196
+
197
+ const isSameTest = (test, t) =>
198
+ typeof t === 'object' &&
199
+ typeof test === 'object' &&
200
+ t.title === test.title &&
201
+ t.suite_title === test.suite_title &&
202
+ Object.values(t.example || {}) === Object.values(test.example || {}) &&
203
+ t.test_id === test.test_id;
204
+
205
+ const getCurrentDateTime = () => {
206
+ const today = new Date();
207
+
208
+ return `${today.getFullYear()}_${
209
+ today.getMonth() + 1
210
+ }_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
211
+ };
212
+
213
+ /**
214
+ * @param {Object} test - Test adapter object
215
+ *
216
+ * @returns {String|null} testInfo as one string
217
+ */
218
+ const specificTestInfo = test => {
219
+ // TODO: afterEach has another context.... need to add specific handler, maybe...
220
+ if (test?.title && test?.file) {
221
+ return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
222
+ }
223
+
224
+ return null;
225
+ };
226
+
227
+ const fileSystem = {
228
+ createDir(dirPath) {
229
+ if (!fs.existsSync(dirPath)) {
230
+ fs.mkdirSync(dirPath, { recursive: true });
231
+ debug('Created dir: ', dirPath);
232
+ }
233
+ },
234
+ clearDir(dirPath) {
235
+ if (fs.existsSync(dirPath)) {
236
+ fs.rmSync(dirPath, { recursive: true });
237
+ debug(`Dir ${dirPath} was deleted`);
238
+ } else {
239
+ debug(`Trying to delete ${dirPath} but it doesn't exist`);
240
+ }
241
+ },
242
+ };
243
+
244
+ const foundedTestLog = (app, tests) => {
245
+ const n = tests.length;
246
+
247
+ return n === 1 ? console.log(app, `✅ We found one test!`) : console.log(app, `✅ We found ${n} tests!`);
248
+ };
249
+
250
+ const humanize = text => {
251
+ // if there are no spaces, decamelize
252
+ if (!text.trim().includes(' ')) text = decamelize(text);
253
+
254
+ return text
255
+ .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
256
+ .trim()
257
+ .replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
258
+ .trim()
259
+ .replace(/\sA\s/g, ' a ') // replace a|the
260
+ .replace(/\sThe\s/g, ' the ') // replace a|the
261
+ .replace(/^Test\s/, '')
262
+ .replace(/^Should\s/, '');
263
+ };
264
+
265
+ /**
266
+ * From https://github.com/sindresorhus/decamelize/blob/main/index.js
267
+ * @param {*} text
268
+ * @returns
269
+ */
270
+ const decamelize = text => {
271
+ const separator = '_';
272
+ const replacement = `$1${separator}$2`;
273
+
274
+ // Split lowercase sequences followed by uppercase character.
275
+ // `dataForUSACounties` → `data_For_USACounties`
276
+ // `myURLstring → `my_URLstring`
277
+ let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
278
+
279
+ // Lowercase all single uppercase characters. As we
280
+ // want to preserve uppercase sequences, we cannot
281
+ // simply lowercase the separated string at the end.
282
+ // `data_For_USACounties` → `data_for_USACounties`
283
+ decamelized = decamelized.replace(
284
+ /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
285
+ $0 => $0.toLowerCase(),
286
+ );
287
+
288
+ // Remaining uppercase sequences will be separated from lowercase sequences.
289
+ // `data_For_USACounties` → `data_for_USA_counties`
290
+ return decamelized.replace(
291
+ /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
292
+ (_, $1, $2) => $1 + separator + $2.toLowerCase(),
293
+ );
294
+ };
295
+
296
+ /**
297
+ * Used to remove color codes
298
+ * @param {*} input
299
+ * @returns
300
+ */
301
+ function removeColorCodes(input) {
302
+ // eslint-disable-next-line no-control-regex
303
+ return input.replace(/\x1b\[[0-9;]*m/g, '');
304
+ }
305
+
306
+ const testRunnerHelper = {
307
+ // for Jest
308
+ getNameOfCurrentlyRunningTest: () => {
309
+ if (global.testomatioTestTitle) return global.testomatioTestTitle;
310
+
311
+ if (!process.env.JEST_WORKER_ID) return null;
312
+ try {
313
+ // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
314
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
315
+ // eslint-disable-next-line no-undef
316
+ return expect?.getState()?.currentTestName;
317
+ } catch (e) {
318
+ return null;
319
+ }
320
+ },
321
+ };
322
+
323
+ export {
324
+ isSameTest,
325
+ fetchSourceCode,
326
+ fetchSourceCodeFromStackTrace,
327
+ fetchIdFromCode,
328
+ fetchIdFromOutput,
329
+ fetchFilesFromStackTrace,
330
+ fileSystem,
331
+ getCurrentDateTime,
332
+ specificTestInfo,
333
+ isValidUrl,
334
+ ansiRegExp,
335
+ getTestomatIdFromTestTitle,
336
+ parseSuite,
337
+ humanize,
338
+ removeColorCodes,
339
+ foundedTestLog,
340
+ testRunnerHelper,
341
+ };