@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.0

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 (62) hide show
  1. package/README.md +3 -2
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +40 -11
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +57 -152
  7. package/lib/data-storage.d.ts +1 -1
  8. package/lib/helpers.d.ts +1 -0
  9. package/lib/helpers.js +4 -0
  10. package/lib/junit-adapter/csharp.d.ts +0 -1
  11. package/lib/junit-adapter/csharp.js +43 -7
  12. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  13. package/lib/junit-adapter/nunit-parser.js +433 -0
  14. package/lib/pipe/bitbucket.js +5 -5
  15. package/lib/pipe/coverage.d.ts +82 -0
  16. package/lib/pipe/coverage.js +373 -0
  17. package/lib/pipe/gitlab.js +4 -4
  18. package/lib/pipe/index.js +2 -0
  19. package/lib/pipe/testomatio.d.ts +3 -2
  20. package/lib/pipe/testomatio.js +44 -18
  21. package/lib/reporter-functions.js +14 -12
  22. package/lib/reporter.d.ts +31 -21
  23. package/lib/reporter.js +40 -5
  24. package/lib/services/artifacts.d.ts +1 -1
  25. package/lib/services/key-values.d.ts +1 -1
  26. package/lib/services/links.d.ts +1 -1
  27. package/lib/services/logger.d.ts +1 -1
  28. package/lib/uploader.js +4 -0
  29. package/lib/utils/log-formatter.d.ts +28 -0
  30. package/lib/utils/log-formatter.js +127 -0
  31. package/lib/utils/pipe_utils.d.ts +15 -0
  32. package/lib/utils/pipe_utils.js +44 -2
  33. package/lib/utils/utils.d.ts +6 -0
  34. package/lib/utils/utils.js +260 -25
  35. package/lib/xmlReader.d.ts +32 -26
  36. package/lib/xmlReader.js +121 -52
  37. package/package.json +12 -7
  38. package/src/adapter/codecept.js +19 -19
  39. package/src/adapter/mocha.js +1 -1
  40. package/src/adapter/playwright.js +2 -2
  41. package/src/bin/cli.js +51 -13
  42. package/src/bin/reportXml.js +5 -2
  43. package/src/client.js +69 -130
  44. package/src/helpers.js +1 -0
  45. package/src/junit-adapter/csharp.js +48 -6
  46. package/src/junit-adapter/nunit-parser.js +474 -0
  47. package/src/pipe/bitbucket.js +5 -5
  48. package/src/pipe/coverage.js +440 -0
  49. package/src/pipe/debug.js +1 -2
  50. package/src/pipe/gitlab.js +4 -4
  51. package/src/pipe/index.js +2 -0
  52. package/src/pipe/testomatio.js +109 -85
  53. package/src/reporter-functions.js +15 -12
  54. package/src/reporter.js +6 -4
  55. package/src/services/links.js +1 -1
  56. package/src/uploader.js +5 -0
  57. package/src/utils/log-formatter.js +113 -0
  58. package/src/utils/pipe_utils.js +52 -3
  59. package/src/utils/utils.js +277 -22
  60. package/src/xmlReader.js +144 -46
  61. package/types/types.d.ts +364 -0
  62. package/types/vitest.types.d.ts +93 -0
@@ -0,0 +1,474 @@
1
+ import createDebugMessages from 'debug';
2
+ import { STATUS } from '../constants.js';
3
+ import { fetchFilesFromStackTrace } from '../utils/utils.js';
4
+
5
+ const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
6
+
7
+ /**
8
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
9
+ * and parameterized tests
10
+ */
11
+ export class NUnitXmlParser {
12
+ constructor(options = {}) {
13
+ this.options = options;
14
+ this.tests = [];
15
+ this.stats = {
16
+ total: 0,
17
+ passed: 0,
18
+ failed: 0,
19
+ skipped: 0,
20
+ inconclusive: 0,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Parse NUnit XML test-run structure
26
+ * @param {Object} testRun - Parsed XML test-run object
27
+ * @returns {Object} - Parsed test results
28
+ */
29
+ parseTestRun(testRun) {
30
+ debug('Parsing NUnit test-run');
31
+
32
+ // Extract run-level statistics
33
+ this.stats = {
34
+ total: parseInt(testRun.total || 0, 10),
35
+ passed: parseInt(testRun.passed || 0, 10),
36
+ failed: parseInt(testRun.failed || 0, 10),
37
+ skipped: parseInt(testRun.skipped || 0, 10),
38
+ inconclusive: parseInt(testRun.inconclusive || 0, 10),
39
+ };
40
+
41
+ // Process the root test-suite
42
+ if (testRun['test-suite']) {
43
+ this.parseTestSuite(testRun['test-suite'], []);
44
+ }
45
+
46
+ debug(`Parsed ${this.tests.length} tests from NUnit XML`);
47
+
48
+ return {
49
+ status: testRun.result?.toLowerCase() || 'unknown',
50
+ create_tests: true,
51
+ tests_count: this.tests.length,
52
+ passed_count: this.tests.filter(t => t.status === STATUS.PASSED).length,
53
+ failed_count: this.tests.filter(t => t.status === STATUS.FAILED).length,
54
+ skipped_count: this.tests.filter(t => t.status === STATUS.SKIPPED).length,
55
+ tests: this.tests,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Recursively parse test-suite elements based on their type
61
+ * @param {Object|Array} testSuite - Test suite object or array
62
+ * @param {Array} parentPath - Current path in the hierarchy
63
+ */
64
+ parseTestSuite(testSuite, parentPath = []) {
65
+ if (!testSuite) return;
66
+
67
+ // Handle arrays of test suites
68
+ if (Array.isArray(testSuite)) {
69
+ testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
70
+ return;
71
+ }
72
+
73
+ const suiteType = testSuite.type;
74
+ const suiteName = testSuite.name;
75
+ const fullName = testSuite.fullname;
76
+
77
+ debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
78
+
79
+ switch (suiteType) {
80
+ case 'Assembly':
81
+ // Assembly level - ignore the name, just process children
82
+ debug('Processing Assembly level - ignoring name, processing children');
83
+ this.processChildren(testSuite, parentPath);
84
+ break;
85
+
86
+ case 'TestSuite':
87
+ // Namespace/grouping level - add to path but don't create test
88
+ debug(`Processing TestSuite level - adding '${suiteName}' to path`);
89
+ // Avoid adding duplicate suite names to the path
90
+ const newPath = parentPath[parentPath.length - 1] === suiteName ? [...parentPath] : [...parentPath, suiteName];
91
+ this.processChildren(testSuite, newPath);
92
+ break;
93
+
94
+ case 'TestFixture':
95
+ // Test class level - add to path and process test cases
96
+ debug(`Processing TestFixture level - test class '${suiteName}'`);
97
+ const testFixturePath = [...parentPath, suiteName];
98
+ this.processChildren(testSuite, testFixturePath);
99
+ break;
100
+
101
+ case 'ParameterizedMethod':
102
+ // Parameterized method level - process test cases directly
103
+ debug(`Processing ParameterizedMethod level - method '${suiteName}'`);
104
+ // Don't add to path, just process children directly
105
+ this.processChildren(testSuite, parentPath);
106
+ break;
107
+
108
+ default:
109
+ debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
110
+ const unknownPath = [...parentPath, suiteName];
111
+ this.processChildren(testSuite, unknownPath);
112
+ break;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Process child elements of a test suite
118
+ * @param {Object} testSuite - Test suite object
119
+ * @param {Array} currentPath - Current path in hierarchy
120
+ */
121
+ processChildren(testSuite, currentPath) {
122
+ // Process test-cases first (to maintain order)
123
+ if (testSuite['test-case']) {
124
+ this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
125
+ }
126
+
127
+ // Process nested test-suites
128
+ if (testSuite['test-suite']) {
129
+ this.parseTestSuite(testSuite['test-suite'], currentPath);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Parse test-case elements (actual tests)
135
+ * @param {Object|Array} testCases - Test case object or array
136
+ * @param {Array} suitePath - Path to the test suite
137
+ * @param {Object} parentSuite - Parent test suite for context
138
+ */
139
+ parseTestCases(testCases, suitePath, parentSuite) {
140
+ if (!testCases) return;
141
+
142
+ // Handle arrays of test cases
143
+ if (!Array.isArray(testCases)) {
144
+ testCases = [testCases];
145
+ }
146
+
147
+ testCases.forEach(testCase => {
148
+ const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
149
+ if (parsedTest) {
150
+ this.tests.push(parsedTest);
151
+ }
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Parse individual test case
157
+ * @param {Object} testCase - Test case object
158
+ * @param {Array} suitePath - Path to the test suite
159
+ * @param {Object} parentSuite - Parent test suite for context
160
+ * @returns {Object|null} - Parsed test object
161
+ */
162
+ parseTestCase(testCase, suitePath, parentSuite) {
163
+ if (!testCase || !testCase.name) {
164
+ debug('Skipping test case without name');
165
+ return null;
166
+ }
167
+
168
+ // Use Description from properties if available (for SpecFlow tests), otherwise use name
169
+ let testName = testCase.name;
170
+ if (testCase.properties && testCase.properties.property) {
171
+ const properties = Array.isArray(testCase.properties.property)
172
+ ? testCase.properties.property
173
+ : [testCase.properties.property];
174
+
175
+ const descriptionProperty = properties.find(p => p.name === 'Description');
176
+ if (descriptionProperty && descriptionProperty.value) {
177
+ // Clean up SpecFlow description format: [C211256] Allow mobile print behavior -> Allow mobile print behavior
178
+ testName = descriptionProperty.value.replace(/^\[[^\]]+\]\s*/, '');
179
+ }
180
+ }
181
+
182
+ const fullName = testCase.fullname;
183
+ const methodName = testCase.methodname || this.extractMethodName(testName);
184
+ const className = testCase.classname || parentSuite?.name;
185
+
186
+ debug(`Parsing test case: ${testName}`);
187
+ debug(`Test case structure:`, JSON.stringify(testCase, null, 2));
188
+
189
+ // Extract parameters if this is a parameterized test
190
+ const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
191
+
192
+ // Determine test status
193
+ let status = STATUS.PASSED;
194
+ if (testCase.result) {
195
+ switch (testCase.result.toLowerCase()) {
196
+ case 'passed':
197
+ status = STATUS.PASSED;
198
+ break;
199
+ case 'failed':
200
+ status = STATUS.FAILED;
201
+ break;
202
+ case 'skipped':
203
+ case 'ignored':
204
+ status = STATUS.SKIPPED;
205
+ break;
206
+ case 'inconclusive':
207
+ status = STATUS.SKIPPED; // Treat inconclusive as skipped
208
+ break;
209
+ default:
210
+ status = STATUS.PASSED;
211
+ }
212
+ }
213
+
214
+ // Extract error information
215
+ let message = '';
216
+ let stack = '';
217
+
218
+ const files = [];
219
+
220
+ // Extract attachments (NUnit format)
221
+ if (testCase.attachments) {
222
+ const attachments = Array.isArray(testCase.attachments.attachment)
223
+ ? testCase.attachments.attachment
224
+ : [testCase.attachments.attachment];
225
+
226
+ const attachmentFiles = attachments.filter(a => a && a.filePath).map(a => a.filePath);
227
+ files.push(...attachmentFiles);
228
+ }
229
+
230
+ if (testCase.failure) {
231
+ message = testCase.failure.message || '';
232
+ stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
233
+ }
234
+
235
+ if (testCase.output) {
236
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
237
+ const stackFiles = fetchFilesFromStackTrace(outputText);
238
+ files.push(...stackFiles);
239
+
240
+ if (outputText) {
241
+ debug(`Found output in test case: ${outputText.substring(0, 100)}...`);
242
+ stack = `${stack}\n\n${outputText}`.trim();
243
+ } else {
244
+ debug('No output text found in test case');
245
+ }
246
+ } else {
247
+ debug('No output found in test case');
248
+ }
249
+
250
+ // Extract test ID and tags from properties
251
+ let testId = null;
252
+ let tags = [];
253
+ if (testCase.properties && testCase.properties.property) {
254
+ const properties = Array.isArray(testCase.properties.property)
255
+ ? testCase.properties.property
256
+ : [testCase.properties.property];
257
+
258
+ const idProperty = properties.find(p => p.name === 'ID');
259
+ if (idProperty) {
260
+ testId = idProperty.value;
261
+ // Remove @ and T prefixes if present
262
+ if (testId.startsWith('@')) testId = testId.slice(1);
263
+ if (testId.startsWith('T')) testId = testId.slice(1);
264
+ }
265
+
266
+ // Extract Category properties as tags
267
+ const categoryProperties = properties.filter(p => p.name === 'Category');
268
+ tags = categoryProperties.map(p => p.value);
269
+ }
270
+
271
+ // If no test ID found in properties, try to extract from output
272
+ if (!testId && testCase.output) {
273
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
274
+ if (outputText) {
275
+ debug(`Looking for test ID in output: ${outputText.substring(0, 200)}...`);
276
+ const idMatch = outputText.match(/\[ID\]\s+tid:\/\/@T([a-f0-9]{8})/i);
277
+ if (idMatch) {
278
+ testId = idMatch[1];
279
+ debug(`Found test ID in output: ${testId}`);
280
+ } else {
281
+ debug('No test ID found in output');
282
+ }
283
+ }
284
+ }
285
+
286
+ // Build file path from suite path and class name
287
+ const filePath = this.buildFilePath(suitePath, className, parentSuite);
288
+
289
+ // For parameterized tests, format example as expected by Testomatio API
290
+ // Convert array of parameters to object with numeric keys
291
+ let example = null;
292
+ if (isParameterized && parameters.length > 0) {
293
+ example = {};
294
+ parameters.forEach((param, index) => {
295
+ example[index] = param;
296
+ });
297
+ }
298
+
299
+ return {
300
+ // For runs: use full test name with parameters (TestBooleanValue(true))
301
+ // For import: API will group by base name using the example field
302
+ title: testName, // Full name with parameters for run display
303
+ methodName: baseMethodName || methodName || testName,
304
+ fullName: fullName,
305
+ suitePath: suitePath,
306
+ suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
307
+ file: filePath,
308
+ files: files, // Array of files that will be attached
309
+ status: status,
310
+ message: message,
311
+ stack: stack,
312
+ run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
313
+ test_id: testId,
314
+ tags: tags, // Array of category tags from properties
315
+ create: true,
316
+ retry: false,
317
+ // Parameterized test metadata
318
+ example: example, // Parameters as object for API grouping
319
+ isParameterized: isParameterized,
320
+ parameters: parameters, // Keep original array for reference
321
+ baseMethodName: baseMethodName,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Extract method name and parameters from test name
327
+ * @param {string} testName - Full test name
328
+ * @returns {Object} - Extracted information
329
+ */
330
+ extractParameters(testName) {
331
+ const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
332
+
333
+ if (paramMatch) {
334
+ const baseMethodName = paramMatch[1].trim();
335
+ const paramString = paramMatch[2];
336
+
337
+ // Parse parameters - handle quoted strings and nested structures
338
+ const parameters = this.parseParameterString(paramString);
339
+
340
+ return {
341
+ baseMethodName: baseMethodName,
342
+ parameters: parameters,
343
+ isParameterized: true,
344
+ };
345
+ }
346
+
347
+ return {
348
+ baseMethodName: testName,
349
+ parameters: [],
350
+ isParameterized: false,
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Parse parameter string into array of parameters
356
+ * @param {string} paramString - Parameter string
357
+ * @returns {Array} - Array of parameters
358
+ */
359
+ parseParameterString(paramString) {
360
+ const parameters = [];
361
+ let current = '';
362
+ let inQuotes = false;
363
+ let quoteChar = null;
364
+ let depth = 0;
365
+
366
+ for (let i = 0; i < paramString.length; i++) {
367
+ const char = paramString[i];
368
+
369
+ if (!inQuotes && (char === '"' || char === "'")) {
370
+ inQuotes = true;
371
+ quoteChar = char;
372
+ current += char;
373
+ } else if (inQuotes && char === quoteChar) {
374
+ inQuotes = false;
375
+ quoteChar = null;
376
+ current += char;
377
+ } else if (!inQuotes && char === '(') {
378
+ depth++;
379
+ current += char;
380
+ } else if (!inQuotes && char === ')') {
381
+ depth--;
382
+ current += char;
383
+ } else if (!inQuotes && char === ',' && depth === 0) {
384
+ parameters.push(current.trim());
385
+ current = '';
386
+ } else {
387
+ current += char;
388
+ }
389
+ }
390
+
391
+ if (current.trim()) {
392
+ parameters.push(current.trim());
393
+ }
394
+
395
+ // Clean up parameters - remove quotes if they wrap the entire parameter and filter empty ones
396
+ return parameters
397
+ .map(param => {
398
+ param = param.trim();
399
+ if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
400
+ return param.slice(1, -1);
401
+ }
402
+ return param;
403
+ })
404
+ .filter(p => !!p);
405
+ }
406
+
407
+ /**
408
+ * Extract method name from test name (fallback)
409
+ * @param {string} testName - Test name
410
+ * @returns {string} - Method name
411
+ */
412
+ extractMethodName(testName) {
413
+ // Remove parameters if present
414
+ const paramMatch = testName.match(/^(.+?)\(/);
415
+ return paramMatch ? paramMatch[1].trim() : testName;
416
+ }
417
+
418
+ /**
419
+ * Build file path from suite path and class name
420
+ * @param {Array} suitePath - Suite path array
421
+ * @param {string} className - Class name
422
+ * @param {Object} parentSuite - Parent suite for context
423
+ * @returns {string} - File path
424
+ */
425
+ buildFilePath(suitePath, className, parentSuite) {
426
+ // Try to get file path from parent suite
427
+ if (parentSuite && parentSuite.filepath) {
428
+ return parentSuite.filepath;
429
+ }
430
+
431
+ // Build path from suite hierarchy
432
+ const pathParts = [...suitePath];
433
+ if (className && !pathParts.includes(className)) {
434
+ pathParts.push(className);
435
+ }
436
+
437
+ // Convert to file path format
438
+ return pathParts.join('/') + '.cs'; // Assume C# for NUnit
439
+ }
440
+
441
+ /**
442
+ * Group parameterized tests by base method name
443
+ * @param {Array} tests - Array of parsed tests
444
+ * @returns {Object} - Grouped tests
445
+ */
446
+ groupParameterizedTests(tests) {
447
+ const grouped = {};
448
+
449
+ tests.forEach(test => {
450
+ const key = test.isParameterized
451
+ ? `${test.suitePath.join('.')}.${test.baseMethodName}`
452
+ : `${test.suitePath.join('.')}.${test.title}`;
453
+
454
+ if (!grouped[key]) {
455
+ grouped[key] = {
456
+ baseTest: {
457
+ name: test.baseMethodName || test.title,
458
+ suitePath: test.suitePath,
459
+ suite_title: test.suite_title,
460
+ file: test.file,
461
+ isParameterized: test.isParameterized,
462
+ },
463
+ variations: [],
464
+ };
465
+ }
466
+
467
+ grouped[key].variations.push(test);
468
+ });
469
+
470
+ return grouped;
471
+ }
472
+ }
473
+
474
+ export default NUnitXmlParser;
@@ -44,8 +44,8 @@ export class BitbucketPipe {
44
44
  baseURL: 'https://api.bitbucket.org/2.0',
45
45
  headers: {
46
46
  'Content-Type': 'application/json',
47
- 'Authorization': `Bearer ${this.token}`
48
- }
47
+ Authorization: `Bearer ${this.token}`,
48
+ },
49
49
  });
50
50
 
51
51
  debug('Bitbucket Pipe: Enabled');
@@ -186,7 +186,7 @@ export class BitbucketPipe {
186
186
  const addCommentResponse = await this.client.request({
187
187
  method: 'POST',
188
188
  url: commentsRequestURL,
189
- data: { content: { raw: body } }
189
+ data: { content: { raw: body } },
190
190
  });
191
191
 
192
192
  const commentID = addCommentResponse.data.id;
@@ -221,7 +221,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
221
221
  try {
222
222
  const response = await client.request({
223
223
  method: 'GET',
224
- url: commentsRequestURL
224
+ url: commentsRequestURL,
225
225
  });
226
226
  comments = response.data.values;
227
227
  } catch (e) {
@@ -238,7 +238,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
238
238
  const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
239
239
  await client.request({
240
240
  method: 'DELETE',
241
- url: deleteCommentURL
241
+ url: deleteCommentURL,
242
242
  });
243
243
  } catch (e) {
244
244
  console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);