@testomatio/reporter 2.3.6 → 2.3.7-beta.2-xml-import

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/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);
@@ -122,12 +122,8 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
122
122
  .map(f => f[1].trim())
123
123
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
124
124
  .map(f => {
125
- // Convert Windows paths to Linux paths for testing purposes
126
- if (f.match(/^[A-Za-z]:[\\\/]/)) {
127
- // Convert Windows path to Linux equivalent for test scenarios
128
- return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
129
- }
130
- return f;
125
+ // Normalize path separators for cross-platform compatibility
126
+ return f.replace(/\\/g, '/');
131
127
  });
132
128
  debug('Found files in stack trace: ', files);
133
129
  return files.filter(f => {
@@ -174,6 +170,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
174
170
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
175
171
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
176
172
  const fetchIdFromCode = (code, opts = {}) => {
173
+ if (!code)
174
+ return null;
177
175
  const comments = code
178
176
  .split('\n')
179
177
  .map(l => l.trim())
@@ -216,10 +214,58 @@ const fetchSourceCode = (contents, opts = {}) => {
216
214
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
217
215
  }
218
216
  else if (opts.lang === 'csharp') {
219
- if (lineIndex === -1)
220
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
221
- if (lineIndex === -1)
222
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
217
+ // Find the method declaration line
218
+ let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
219
+ if (methodLineIndex === -1) {
220
+ methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
221
+ }
222
+ if (methodLineIndex === -1) {
223
+ methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
224
+ }
225
+ // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
226
+ if (methodLineIndex !== -1) {
227
+ lineIndex = methodLineIndex;
228
+ // Scan upwards to find the start of attributes and comments
229
+ for (let i = methodLineIndex - 1; i >= 0; i--) {
230
+ const trimmedLine = lines[i].trim();
231
+ // Include [TestCase], [Test], and other attributes
232
+ if (trimmedLine.startsWith('[')) {
233
+ lineIndex = i;
234
+ continue;
235
+ }
236
+ // Include XML documentation comments
237
+ if (trimmedLine.startsWith('///')) {
238
+ lineIndex = i;
239
+ continue;
240
+ }
241
+ // Stop at empty lines (with some tolerance)
242
+ if (trimmedLine === '') {
243
+ // Check if next non-empty line is an attribute or comment
244
+ let hasMoreAttributes = false;
245
+ for (let j = i - 1; j >= 0; j--) {
246
+ const nextTrimmed = lines[j].trim();
247
+ if (nextTrimmed === '')
248
+ continue;
249
+ if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
250
+ hasMoreAttributes = true;
251
+ lineIndex = j;
252
+ }
253
+ break;
254
+ }
255
+ if (!hasMoreAttributes)
256
+ break;
257
+ continue;
258
+ }
259
+ // Stop at other method declarations or class-level elements
260
+ if (trimmedLine.includes('public ') ||
261
+ trimmedLine.includes('private ') ||
262
+ trimmedLine.includes('protected ') ||
263
+ trimmedLine.includes('internal ')) {
264
+ if (!trimmedLine.startsWith('['))
265
+ break;
266
+ }
267
+ }
268
+ }
223
269
  }
224
270
  else {
225
271
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -228,11 +274,28 @@ const fetchSourceCode = (contents, opts = {}) => {
228
274
  if (opts.prepend) {
229
275
  lineIndex -= opts.prepend;
230
276
  }
231
- if (lineIndex) {
277
+ if (lineIndex !== -1 && lineIndex !== undefined) {
232
278
  const result = [];
279
+ let braceDepth = 0; // Track brace depth for C# methods
280
+ let methodStartFound = false; // Flag to indicate we've found the method opening brace
233
281
  for (let i = lineIndex; i < lineIndex + limit; i++) {
234
282
  if (lines[i] === undefined)
235
283
  continue;
284
+ // Track brace depth for C# to stop after method closes
285
+ if (opts.lang === 'csharp') {
286
+ const line = lines[i];
287
+ // Count opening and closing braces
288
+ const openBraces = (line.match(/\{/g) || []).length;
289
+ const closeBraces = (line.match(/\}/g) || []).length;
290
+ if (openBraces > 0)
291
+ methodStartFound = true;
292
+ braceDepth += openBraces - closeBraces;
293
+ // If we've started the method and depth returns to 0, method is complete
294
+ if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
295
+ result.push(lines[i]);
296
+ break;
297
+ }
298
+ }
236
299
  if (i > lineIndex + 2 && !opts.prepend) {
237
300
  // annotation
238
301
  if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
@@ -271,6 +334,22 @@ const fetchSourceCode = (contents, opts = {}) => {
271
334
  break;
272
335
  if (opts.lang === 'java' && lines[i].includes(' class '))
273
336
  break;
337
+ // For C#, additional checks if brace tracking didn't stop us
338
+ if (opts.lang === 'csharp') {
339
+ const trimmed = lines[i].trim();
340
+ // Stop at attribute that marks beginning of next test
341
+ if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/))
342
+ break;
343
+ // Stop at XML documentation comments that belong to next method
344
+ if (trimmed.startsWith('///'))
345
+ break;
346
+ // Stop at another method declaration
347
+ if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/))
348
+ break;
349
+ // Stop at class declaration
350
+ if (trimmed.includes(' class ') && trimmed.includes('public'))
351
+ break;
352
+ }
274
353
  }
275
354
  result.push(lines[i]);
276
355
  }
@@ -19,25 +19,11 @@ declare class XmlReader {
19
19
  tests: any[];
20
20
  stats: {};
21
21
  uploader: S3Uploader;
22
+ enhancedNunit: boolean;
23
+ groupParameterized: boolean;
22
24
  version: any;
23
25
  connectAdapter(): import("./junit-adapter/adapter.js").default;
24
- parse(fileName: any): {
25
- status: string;
26
- create_tests: boolean;
27
- tests_count: number;
28
- passed_count: number;
29
- skipped_count: number;
30
- failed_count: number;
31
- tests: any;
32
- } | {
33
- status: any;
34
- create_tests: boolean;
35
- tests_count: number;
36
- passed_count: number;
37
- failed_count: number;
38
- skipped_count: number;
39
- tests: any[];
40
- };
26
+ parse(fileName: any): any;
41
27
  processJUnit(jsonSuite: any): {
42
28
  create_tests: boolean;
43
29
  duration: number;
@@ -49,15 +35,14 @@ declare class XmlReader {
49
35
  tests: any[];
50
36
  tests_count: number;
51
37
  };
52
- processNUnit(jsonSuite: any): {
53
- status: any;
54
- create_tests: boolean;
55
- tests_count: number;
56
- passed_count: number;
57
- failed_count: number;
58
- skipped_count: number;
59
- tests: any[];
60
- };
38
+ processNUnit(jsonSuite: any): any;
39
+ /**
40
+ * Check if the XML is actually NUnit format (has test-suite hierarchy)
41
+ * @param {Object} jsonSuite - Parsed XML suite object
42
+ * @returns {boolean} - True if this is NUnit XML format
43
+ */
44
+ isNUnitXml(jsonSuite: any): boolean;
45
+ processNUnitEnhanced(jsonSuite: any): any;
61
46
  processTRX(jsonSuite: any): {
62
47
  status: string;
63
48
  create_tests: boolean;
@@ -67,6 +52,27 @@ declare class XmlReader {
67
52
  failed_count: number;
68
53
  tests: any;
69
54
  };
55
+ _parseTRXTestDefinition(td: any): {
56
+ title: any;
57
+ example: any;
58
+ file: string;
59
+ description: any;
60
+ suite_title: any;
61
+ id: any;
62
+ };
63
+ _parseTRXTestResult(td: any, tests: any): {
64
+ suite_title: any;
65
+ title: any;
66
+ file: any;
67
+ description: any;
68
+ code: any;
69
+ run_time: number;
70
+ stack: any;
71
+ files: any;
72
+ create: boolean;
73
+ overwrite: boolean;
74
+ };
75
+ _mapTRXStatus(outcome: any): string;
70
76
  processXUnit(assemblies: any): {
71
77
  status: string;
72
78
  create_tests: boolean;
package/lib/xmlReader.js CHANGED
@@ -11,6 +11,7 @@ const fast_xml_parser_1 = require("fast-xml-parser");
11
11
  const constants_js_1 = require("./constants.js");
12
12
  const crypto_1 = require("crypto");
13
13
  const url_1 = require("url");
14
+ const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
14
15
  const utils_js_1 = require("./utils/utils.js");
15
16
  const index_js_1 = require("./pipe/index.js");
16
17
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
@@ -54,6 +55,9 @@ class XmlReader {
54
55
  this.stats = {};
55
56
  this.stats.language = opts.lang?.toLowerCase();
56
57
  this.uploader = new uploader_js_1.S3Uploader();
58
+ // Enhanced NUnit parsing - enabled by default for NUnit XML
59
+ this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
60
+ this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
57
61
  // @ts-ignore
58
62
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
59
63
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
@@ -102,7 +106,8 @@ class XmlReader {
102
106
  return this.processJUnit(jsonSuite);
103
107
  }
104
108
  processJUnit(jsonSuite) {
105
- const { testsuite, name, tests, failures, errors } = jsonSuite;
109
+ const { testsuite, name, failures, errors } = jsonSuite;
110
+ const tests = testsuite?.tests || jsonSuite.tests;
106
111
  reduceOptions.preferClassname = this.stats.language === 'python';
107
112
  const resultTests = processTestSuite(testsuite);
108
113
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
@@ -128,6 +133,13 @@ class XmlReader {
128
133
  };
129
134
  }
130
135
  processNUnit(jsonSuite) {
136
+ // Use enhanced NUnit parser if enabled and this is actually NUnit XML
137
+ if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
138
+ debug('Using enhanced NUnit parser');
139
+ return this.processNUnitEnhanced(jsonSuite);
140
+ }
141
+ // Fallback to legacy parser for backward compatibility
142
+ debug('Using legacy NUnit parser');
131
143
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
144
  reduceOptions.preferClassname = this.stats.language === 'python';
133
145
  const resultTests = processTestSuite(jsonSuite['test-suite']);
@@ -142,65 +154,58 @@ class XmlReader {
142
154
  tests: resultTests,
143
155
  };
144
156
  }
157
+ /**
158
+ * Check if the XML is actually NUnit format (has test-suite hierarchy)
159
+ * @param {Object} jsonSuite - Parsed XML suite object
160
+ * @returns {boolean} - True if this is NUnit XML format
161
+ */
162
+ isNUnitXml(jsonSuite) {
163
+ // NUnit XML has test-suite elements with type attributes
164
+ if (jsonSuite['test-suite']) {
165
+ const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
166
+ // Check for NUnit-specific test-suite types
167
+ return (testSuite &&
168
+ testSuite.type &&
169
+ ['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
170
+ }
171
+ return false;
172
+ }
173
+ processNUnitEnhanced(jsonSuite) {
174
+ debug('Processing NUnit XML with enhanced parser');
175
+ try {
176
+ const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
177
+ groupParameterized: this.groupParameterized,
178
+ ...this.opts,
179
+ });
180
+ const result = nunitParser.parseTestRun(jsonSuite);
181
+ // Add parsed tests to our collection
182
+ this.tests = this.tests.concat(result.tests);
183
+ debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
184
+ return result;
185
+ }
186
+ catch (error) {
187
+ debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
188
+ console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
189
+ // Fallback to legacy parser
190
+ this.enhancedNunit = false;
191
+ return this.processNUnit(jsonSuite);
192
+ }
193
+ }
145
194
  processTRX(jsonSuite) {
146
195
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
147
196
  if (!Array.isArray(defs))
148
197
  defs = [defs].filter(d => !!d);
149
- const tests = defs.map(td => {
150
- const title = td.name.replace(/\(.*?\)/, '').trim();
151
- let example = td.name.match(/\((.*?)\)/);
152
- if (example)
153
- example = { ...example[1].split(',') };
154
- const suite = td.TestMethod.className.split(', ')[0].split('.');
155
- const suite_title = suite.pop();
156
- return {
157
- title,
158
- example,
159
- file: suite.join('/'),
160
- description: td.Description,
161
- suite_title,
162
- id: td.Execution.id,
163
- };
164
- }) || [];
198
+ // Parse test definitions
199
+ const tests = defs.map(td => this._parseTRXTestDefinition(td));
200
+ // Parse test results
165
201
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
166
202
  if (!Array.isArray(result))
167
203
  result = [result].filter(d => !!d);
168
- const results = result.map(td => ({
169
- id: td.executionId,
170
- // seconds are used in junit reports, but ms are used by testomatio
171
- run_time: parseFloat(td.duration) * 1000,
172
- status: td.outcome,
173
- stack: td.Output.StdOut,
174
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
175
- }));
176
- results.forEach(r => {
177
- const test = tests.find(t => t.id === r.id) || {};
178
- r.suite_title = test.suite_title;
179
- r.title = test.title?.trim();
180
- if (test.code)
181
- r.code = test.code;
182
- if (test.description)
183
- r.description = test.description;
184
- if (test.example)
185
- r.example = test.example;
186
- if (test.file)
187
- r.file = test.file;
188
- r.create = true;
189
- r.overwrite = true;
190
- if (r.status === 'Passed')
191
- r.status = constants_js_1.STATUS.PASSED;
192
- if (r.status === 'Failed')
193
- r.status = constants_js_1.STATUS.FAILED;
194
- if (r.status === 'Skipped')
195
- r.status = constants_js_1.STATUS.SKIPPED;
196
- delete r.id;
197
- });
204
+ const results = result.map(td => this._parseTRXTestResult(td, tests));
198
205
  debug(results);
199
206
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
200
207
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
201
- let status = constants_js_1.STATUS.PASSED.toString();
202
- if (failed_count > 0)
203
- status = constants_js_1.STATUS.FAILED;
208
+ const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
204
209
  this.tests = results.filter(t => !!t.title);
205
210
  return {
206
211
  status,
@@ -212,6 +217,57 @@ class XmlReader {
212
217
  tests: results,
213
218
  };
214
219
  }
220
+ _parseTRXTestDefinition(td) {
221
+ const title = td.name.replace(/\(.*?\)/, '').trim();
222
+ const exampleMatch = td.name.match(/\((.*?)\)/);
223
+ const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
224
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
225
+ const suite_title = suite.pop();
226
+ // Convert namespace to file path for C#
227
+ const file = `${suite.join('/')}.cs`;
228
+ return {
229
+ title, // Base name without parameters for test import
230
+ example, // Parameters object for parameterized tests
231
+ file, // File path with .cs extension
232
+ description: td.Description,
233
+ suite_title,
234
+ id: td.Execution.id,
235
+ };
236
+ }
237
+ _parseTRXTestResult(td, tests) {
238
+ const test = tests.find(t => t.id === td.executionId) || {};
239
+ const result = {
240
+ suite_title: test.suite_title,
241
+ title: test.title?.trim(),
242
+ file: test.file,
243
+ description: test.description,
244
+ code: test.code,
245
+ run_time: parseFloat(td.duration) * 1000,
246
+ stack: td.Output?.StdOut || '',
247
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
248
+ create: true,
249
+ overwrite: true,
250
+ };
251
+ // Add example for parameterized tests
252
+ if (test.example) {
253
+ result.example = test.example;
254
+ }
255
+ // Map TRX status to Testomat.io status
256
+ result.status = this._mapTRXStatus(td.outcome);
257
+ return result;
258
+ }
259
+ _mapTRXStatus(outcome) {
260
+ switch (outcome) {
261
+ case 'Passed':
262
+ return constants_js_1.STATUS.PASSED;
263
+ case 'Failed':
264
+ return constants_js_1.STATUS.FAILED;
265
+ case 'Skipped':
266
+ return constants_js_1.STATUS.SKIPPED;
267
+ default:
268
+ return constants_js_1.STATUS.PASSED;
269
+ }
270
+ }
215
271
  processXUnit(assemblies) {
216
272
  const tests = [];
217
273
  assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.6",
3
+ "version": "2.3.7-beta.2-xml-import",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
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
@@ -37,9 +37,8 @@ class Client {
37
37
  this.runId = '';
38
38
  this.queue = Promise.resolve();
39
39
 
40
- // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
41
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
- const pathToPackageJSON = path.join(__dirname, '../package.json');
40
+ // Get package.json path - use a simple approach that works in both environments
41
+ const pathToPackageJSON = path.join(process.cwd(), 'package.json');
43
42
  try {
44
43
  this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
45
44
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -387,7 +386,11 @@ class Client {
387
386
  */
388
387
  formatLogs({ error, steps, logs }) {
389
388
  error = error?.trim();
390
- logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
389
+ logs = logs
390
+ ?.trim()
391
+ .split('\n')
392
+ .map(l => truncate(l))
393
+ .join('\n');
391
394
 
392
395
  if (Array.isArray(steps)) {
393
396
  steps = steps
@@ -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
+ // For runs: keep full title with parameters for display
20
+ // The example field will be used for grouping on import
21
+ // Do NOT remove parameters from title
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
  }