@testomatio/reporter 2.3.7 → 2.3.8

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