@testomatio/reporter 2.3.7-beta.10-stack-artifacts → 2.3.7-beta.100

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.
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.7-beta.10-stack-artifacts",
3
+ "version": "2.3.7-beta.100",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
7
7
  },
8
- "typings": "typings/index.d.ts",
8
+ "main": "lib/reporter.js",
9
+ "module": "src/reporter.js",
10
+ "types": "types/types.d.ts",
9
11
  "repository": "git@github.com:testomatio/reporter.git",
10
12
  "author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
11
13
  "license": "MIT",
@@ -45,7 +47,8 @@
45
47
  "bin",
46
48
  "lib",
47
49
  "src",
48
- "testcafe"
50
+ "testcafe",
51
+ "types"
49
52
  ],
50
53
  "scripts": {
51
54
  "clear-exportdir": "rm -rf export/",
@@ -57,7 +60,8 @@
57
60
  "test": "mocha 'tests/unit/**/*_test.js'",
58
61
  "test:playwright": "mocha tests/adapter/playwright.test.js",
59
62
  "test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
60
- "test:frameworks": "npm run test:playwright && npm run test:codecept",
63
+ "test:vitest": "mocha tests/adapter/vitest.test.js",
64
+ "test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest",
61
65
  "test:all": "npm run test && npm run test:frameworks",
62
66
  "test:adapters": "mocha tests/adapter/*.test.js",
63
67
  "test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
package/src/bin/cli.js CHANGED
@@ -158,7 +158,7 @@ program
158
158
  .option('--lang <lang>', 'Language used (python, ruby, java)')
159
159
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
160
160
  .action(async (pattern, opts) => {
161
- if (!pattern.endsWith('.xml')) {
161
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
162
162
  pattern += '.xml';
163
163
  }
164
164
  let { javaTests, lang } = opts;
@@ -23,7 +23,7 @@ program
23
23
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
24
24
  .option('--env-file <envfile>', 'Load environment variables from env file')
25
25
  .action(async (pattern, opts) => {
26
- if (!pattern.endsWith('.xml')) {
26
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
27
27
  pattern += '.xml';
28
28
  }
29
29
  let { javaTests, lang } = opts;
@@ -34,7 +34,10 @@ program
34
34
  }
35
35
  lang = lang?.toLowerCase();
36
36
  if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
37
- const runReader = new XmlReader({ javaTests, lang });
37
+ const runReader = new XmlReader({
38
+ javaTests,
39
+ lang,
40
+ });
38
41
  const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
39
42
  if (!files.length) {
40
43
  console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
@@ -18,7 +18,7 @@ const newArgs = ['run'];
18
18
  let i = 0;
19
19
  while (i < args.length) {
20
20
  const arg = args[i];
21
-
21
+
22
22
  if (arg === '-c' || arg === '--command') {
23
23
  // Map -c/--command to positional argument for run command
24
24
  i++;
@@ -33,7 +33,7 @@ while (i < args.length) {
33
33
  // Map --launch to start command
34
34
  newArgs[0] = 'start';
35
35
  } else if (arg === '--finish') {
36
- // Map --finish to finish command
36
+ // Map --finish to finish command
37
37
  newArgs[0] = 'finish';
38
38
  } else {
39
39
  // Pass through other arguments
@@ -45,9 +45,9 @@ while (i < args.length) {
45
45
  // Execute the main CLI with mapped arguments
46
46
 
47
47
  const child = spawn(process.execPath, [cliPath, ...newArgs], {
48
- stdio: 'inherit'
48
+ stdio: 'inherit',
49
49
  });
50
50
 
51
- child.on('exit', (code) => {
51
+ child.on('exit', code => {
52
52
  process.exit(code);
53
- });
53
+ });
package/src/client.js CHANGED
@@ -170,12 +170,12 @@ class Client {
170
170
  title,
171
171
  suite_title,
172
172
  } = testData;
173
- const steps = originalSteps;
173
+ let steps = originalSteps;
174
174
 
175
175
  const uploadedFiles = [];
176
176
  const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
177
177
 
178
-
178
+
179
179
  const {
180
180
  time = 0,
181
181
  example = null,
@@ -210,20 +210,18 @@ class Client {
210
210
 
211
211
  let fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
212
212
 
213
- if (stackArtifactsEnabled && status && (steps || testData.logs || error)) {
214
- const timestamp = +new Date;
215
- if (fullLogs && fullLogs.trim().length > 0) {
216
- uploadedFiles.push(
217
- this.uploader.uploadFileAsBuffer(
218
- Buffer.from(stripColors(fullLogs), 'utf8'),
219
- [this.runId, rid, `logs_${timestamp}.txt`]
220
- )
221
- );
222
- }
213
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
214
+ uploadedFiles.push(
215
+ this.uploader.uploadFileAsBuffer(
216
+ Buffer.from(stripColors(fullLogs), 'utf8'),
217
+ [this.runId, rid, `logs_${+new Date}.log`]
218
+ )
219
+ );
223
220
  fullLogs = '';
221
+ steps = null;
224
222
  }
225
223
 
226
-
224
+
227
225
  if (!this.pipes || !this.pipes.length)
228
226
  this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
229
227
 
@@ -3,18 +3,50 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- const title = t.title.replace(/\(.*?\)/, '').trim();
7
- const example = t.title.match(/\((.*?)\)/);
8
- if (example) t.example = { ...example[1].split(',') };
6
+ // Extract example from title if not already present
7
+ if (!t.example) {
8
+ const exampleMatch = t.title.match(/\((.*?)\)/);
9
+ if (exampleMatch) {
10
+ // Extract parameters as object with numeric keys for API
11
+ const params = exampleMatch[1].split(',').map(param => param.trim());
12
+ t.example = {};
13
+ params.forEach((param, index) => {
14
+ t.example[index] = param;
15
+ });
16
+ }
17
+ }
18
+
19
+ // Remove parameters from title to avoid duplicates in Test Suite
20
+ // The example field will be used for grouping on import
21
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
22
+
9
23
  const suite = t.suite_title.split('.');
10
24
  t.suite_title = suite.pop();
11
25
  t.file = namespaceToFileName(t.file);
12
- t.title = title.trim();
13
26
  return t;
14
27
  }
15
28
 
16
29
  getFilePath(t) {
17
- const fileName = namespaceToFileName(t.file);
30
+ if (!t.file) return null;
31
+
32
+ // Normalize path separators for cross-platform compatibility
33
+ let filePath = t.file.replace(/\\/g, '/');
34
+
35
+ // If file already has .cs extension, use it directly
36
+ if (filePath.endsWith('.cs')) {
37
+ // Make relative path if it's absolute
38
+ if (path.isAbsolute(filePath)) {
39
+ // Try to find project-relative path
40
+ const cwd = process.cwd().replace(/\\/g, '/');
41
+ if (filePath.startsWith(cwd)) {
42
+ filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
43
+ }
44
+ }
45
+ return filePath;
46
+ }
47
+
48
+ // Convert namespace path to file path
49
+ const fileName = namespaceToFileName(filePath);
18
50
  return fileName;
19
51
  }
20
52
  }
@@ -22,7 +54,14 @@ class CSharpAdapter extends Adapter {
22
54
  export default CSharpAdapter;
23
55
 
24
56
  function namespaceToFileName(fileName) {
57
+ if (!fileName) return '';
58
+
59
+ // If already a .cs file path, clean it up
60
+ if (fileName.endsWith('.cs')) {
61
+ return fileName.replace(/\\/g, '/');
62
+ }
63
+
25
64
  const fileParts = fileName.split('.');
26
65
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
27
- return `${fileParts.join(path.sep)}.cs`;
66
+ return `${fileParts.join('/')}.cs`;
28
67
  }
@@ -0,0 +1,462 @@
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
+ if (testCase.failure) {
221
+ message = testCase.failure.message || '';
222
+ stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
223
+ }
224
+
225
+ if (testCase.output) {
226
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
227
+ const stackFiles = fetchFilesFromStackTrace(outputText);
228
+ files.push(...stackFiles);
229
+
230
+ if (outputText) {
231
+ debug(`Found output in test case: ${outputText.substring(0, 100)}...`);
232
+ stack = `${stack}\n\n${outputText}`.trim();
233
+ } else {
234
+ debug('No output text found in test case');
235
+ }
236
+ } else {
237
+ debug('No output found in test case');
238
+ }
239
+
240
+ // Extract test ID and tags from properties
241
+ let testId = null;
242
+ let tags = [];
243
+ if (testCase.properties && testCase.properties.property) {
244
+ const properties = Array.isArray(testCase.properties.property)
245
+ ? testCase.properties.property
246
+ : [testCase.properties.property];
247
+
248
+ const idProperty = properties.find(p => p.name === 'ID');
249
+ if (idProperty) {
250
+ testId = idProperty.value;
251
+ // Remove @ and T prefixes if present
252
+ if (testId.startsWith('@')) testId = testId.slice(1);
253
+ if (testId.startsWith('T')) testId = testId.slice(1);
254
+ }
255
+
256
+ // Extract Category properties as tags
257
+ const categoryProperties = properties.filter(p => p.name === 'Category');
258
+ tags = categoryProperties.map(p => p.value);
259
+ }
260
+
261
+ // If no test ID found in properties, try to extract from output
262
+ if (!testId && testCase.output) {
263
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
264
+ if (outputText) {
265
+ debug(`Looking for test ID in output: ${outputText.substring(0, 200)}...`);
266
+ const idMatch = outputText.match(/\[ID\]\s+tid:\/\/@T([a-f0-9]{8})/i);
267
+ if (idMatch) {
268
+ testId = idMatch[1];
269
+ debug(`Found test ID in output: ${testId}`);
270
+ } else {
271
+ debug('No test ID found in output');
272
+ }
273
+ }
274
+ }
275
+
276
+ // Build file path from suite path and class name
277
+ const filePath = this.buildFilePath(suitePath, className, parentSuite);
278
+
279
+ // For parameterized tests, format example as expected by Testomatio API
280
+ // Convert array of parameters to object with numeric keys
281
+ let example = null;
282
+ if (isParameterized && parameters.length > 0) {
283
+ example = {};
284
+ parameters.forEach((param, index) => {
285
+ example[index] = param;
286
+ });
287
+ }
288
+
289
+ return {
290
+ // For runs: use full test name with parameters (TestBooleanValue(true))
291
+ // For import: API will group by base name using the example field
292
+ title: testName, // Full name with parameters for run display
293
+ methodName: baseMethodName || methodName || testName,
294
+ fullName: fullName,
295
+ suitePath: suitePath,
296
+ suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
297
+ file: filePath,
298
+ files: files, // Array of files that will be attached
299
+ status: status,
300
+ message: message,
301
+ stack: stack,
302
+ run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
303
+ test_id: testId,
304
+ tags: tags, // Array of category tags from properties
305
+ create: true,
306
+ retry: false,
307
+ // Parameterized test metadata
308
+ example: example, // Parameters as object for API grouping
309
+ isParameterized: isParameterized,
310
+ parameters: parameters, // Keep original array for reference
311
+ baseMethodName: baseMethodName,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Extract method name and parameters from test name
317
+ * @param {string} testName - Full test name
318
+ * @returns {Object} - Extracted information
319
+ */
320
+ extractParameters(testName) {
321
+ const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
322
+
323
+ if (paramMatch) {
324
+ const baseMethodName = paramMatch[1].trim();
325
+ const paramString = paramMatch[2];
326
+
327
+ // Parse parameters - handle quoted strings and nested structures
328
+ const parameters = this.parseParameterString(paramString);
329
+
330
+ return {
331
+ baseMethodName: baseMethodName,
332
+ parameters: parameters,
333
+ isParameterized: true,
334
+ };
335
+ }
336
+
337
+ return {
338
+ baseMethodName: testName,
339
+ parameters: [],
340
+ isParameterized: false,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Parse parameter string into array of parameters
346
+ * @param {string} paramString - Parameter string
347
+ * @returns {Array} - Array of parameters
348
+ */
349
+ parseParameterString(paramString) {
350
+ const parameters = [];
351
+ let current = '';
352
+ let inQuotes = false;
353
+ let quoteChar = null;
354
+ let depth = 0;
355
+
356
+ for (let i = 0; i < paramString.length; i++) {
357
+ const char = paramString[i];
358
+
359
+ if (!inQuotes && (char === '"' || char === "'")) {
360
+ inQuotes = true;
361
+ quoteChar = char;
362
+ current += char;
363
+ } else if (inQuotes && char === quoteChar) {
364
+ inQuotes = false;
365
+ quoteChar = null;
366
+ current += char;
367
+ } else if (!inQuotes && char === '(') {
368
+ depth++;
369
+ current += char;
370
+ } else if (!inQuotes && char === ')') {
371
+ depth--;
372
+ current += char;
373
+ } else if (!inQuotes && char === ',' && depth === 0) {
374
+ parameters.push(current.trim());
375
+ current = '';
376
+ } else {
377
+ current += char;
378
+ }
379
+ }
380
+
381
+ if (current.trim()) {
382
+ parameters.push(current.trim());
383
+ }
384
+
385
+ // Clean up parameters - remove quotes if they wrap the entire parameter
386
+ return parameters.map(param => {
387
+ param = param.trim();
388
+ if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
389
+ return param.slice(1, -1);
390
+ }
391
+ return param;
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Extract method name from test name (fallback)
397
+ * @param {string} testName - Test name
398
+ * @returns {string} - Method name
399
+ */
400
+ extractMethodName(testName) {
401
+ // Remove parameters if present
402
+ const paramMatch = testName.match(/^(.+?)\(/);
403
+ return paramMatch ? paramMatch[1].trim() : testName;
404
+ }
405
+
406
+ /**
407
+ * Build file path from suite path and class name
408
+ * @param {Array} suitePath - Suite path array
409
+ * @param {string} className - Class name
410
+ * @param {Object} parentSuite - Parent suite for context
411
+ * @returns {string} - File path
412
+ */
413
+ buildFilePath(suitePath, className, parentSuite) {
414
+ // Try to get file path from parent suite
415
+ if (parentSuite && parentSuite.filepath) {
416
+ return parentSuite.filepath;
417
+ }
418
+
419
+ // Build path from suite hierarchy
420
+ const pathParts = [...suitePath];
421
+ if (className && !pathParts.includes(className)) {
422
+ pathParts.push(className);
423
+ }
424
+
425
+ // Convert to file path format
426
+ return pathParts.join('/') + '.cs'; // Assume C# for NUnit
427
+ }
428
+
429
+ /**
430
+ * Group parameterized tests by base method name
431
+ * @param {Array} tests - Array of parsed tests
432
+ * @returns {Object} - Grouped tests
433
+ */
434
+ groupParameterizedTests(tests) {
435
+ const grouped = {};
436
+
437
+ tests.forEach(test => {
438
+ const key = test.isParameterized
439
+ ? `${test.suitePath.join('.')}.${test.baseMethodName}`
440
+ : `${test.suitePath.join('.')}.${test.title}`;
441
+
442
+ if (!grouped[key]) {
443
+ grouped[key] = {
444
+ baseTest: {
445
+ name: test.baseMethodName || test.title,
446
+ suitePath: test.suitePath,
447
+ suite_title: test.suite_title,
448
+ file: test.file,
449
+ isParameterized: test.isParameterized,
450
+ },
451
+ variations: [],
452
+ };
453
+ }
454
+
455
+ grouped[key].variations.push(test);
456
+ });
457
+
458
+ return grouped;
459
+ }
460
+ }
461
+
462
+ export default NUnitXmlParser;
@@ -472,7 +472,7 @@ class TestomatioPipe {
472
472
  if (this.runUrl && this.proceed) {
473
473
  const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
474
474
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
475
- console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
475
+ console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
476
476
  }
477
477
 
478
478
  if (this.hasUnmatchedTests) {