@testomatio/reporter 2.1.3-beta.2-xml-import → 2.2.1

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.
@@ -172,7 +172,8 @@ class TestomatioPipe {
172
172
  const resp = await this.client.request({
173
173
  method: 'PUT',
174
174
  url: `/api/reporter/${this.runId}`,
175
- data: runParams
175
+ data: runParams,
176
+ responseType: 'json'
176
177
  });
177
178
  if (resp.data.artifacts)
178
179
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
@@ -77,13 +77,6 @@ declare class XmlReader {
77
77
  skipped_count: number;
78
78
  tests: any[];
79
79
  };
80
- deduplicateTestsByFQN(tests: any): any[];
81
- generateFQN(test: any): string;
82
- generateNormalizedFQN(test: any): string;
83
- extractAssemblyName(test: any): any;
84
- extractNamespace(test: any): any;
85
- extractClassName(test: any): any;
86
- extractCsFileFromPath(test: any): any;
87
80
  calculateStats(): {};
88
81
  fetchSourceCode(): void;
89
82
  formatTests(): void;
package/lib/xmlReader.js CHANGED
@@ -131,9 +131,7 @@ class XmlReader {
131
131
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
132
  reduceOptions.preferClassname = this.stats.language === 'python';
133
133
  const resultTests = processTestSuite(jsonSuite['test-suite']);
134
- // Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
135
- const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
136
- this.tests = this.tests.concat(deduplicatedTests);
134
+ this.tests = this.tests.concat(resultTests);
137
135
  return {
138
136
  status: result?.toLowerCase(),
139
137
  create_tests: true,
@@ -141,7 +139,7 @@ class XmlReader {
141
139
  passed_count: parseInt(passed, 10),
142
140
  failed_count: parseInt(failed, 10),
143
141
  skipped_count: parseInt(inconclusive + skipped, 10),
144
- tests: deduplicatedTests,
142
+ tests: resultTests,
145
143
  };
146
144
  }
147
145
  processTRX(jsonSuite) {
@@ -277,169 +275,6 @@ class XmlReader {
277
275
  tests,
278
276
  };
279
277
  }
280
- deduplicateTestsByFQN(tests) {
281
- const fqnMap = new Map();
282
- tests.forEach(test => {
283
- const fqn = this.generateNormalizedFQN(test);
284
- if (fqnMap.has(fqn)) {
285
- const existingTest = fqnMap.get(fqn);
286
- // For parameterized tests, merge as Examples
287
- if (test.example) {
288
- // Initialize examples array if it doesn't exist
289
- if (!existingTest.examples) {
290
- existingTest.examples = [];
291
- // Add the existing test's example as the first item
292
- if (existingTest.example) {
293
- existingTest.examples.push({
294
- parameters: existingTest.example,
295
- status: existingTest.status,
296
- run_time: existingTest.run_time,
297
- message: existingTest.message,
298
- stack: existingTest.stack
299
- });
300
- }
301
- }
302
- // Add this test's execution as an example
303
- existingTest.examples.push({
304
- parameters: test.example,
305
- status: test.status,
306
- run_time: test.run_time,
307
- message: test.message,
308
- stack: test.stack
309
- });
310
- // Update the main test status to reflect the worst status
311
- if (test.status === 'failed' || existingTest.status === 'failed') {
312
- existingTest.status = 'failed';
313
- }
314
- else if (test.status === 'skipped' && existingTest.status !== 'failed') {
315
- existingTest.status = 'skipped';
316
- }
317
- // Update total run time
318
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
319
- // Merge stack traces if they're different
320
- if (test.stack && test.stack !== existingTest.stack) {
321
- existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
322
- }
323
- // Merge messages if they're different
324
- if (test.message && test.message !== existingTest.message) {
325
- existingTest.message = existingTest.message + '; ' + test.message;
326
- }
327
- }
328
- else {
329
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
330
- if (test.test_id && !existingTest.test_id) {
331
- existingTest.test_id = test.test_id;
332
- }
333
- // Keep the most complete test data
334
- if (test.stack && !existingTest.stack) {
335
- existingTest.stack = test.stack;
336
- }
337
- if (test.message && !existingTest.message) {
338
- existingTest.message = test.message;
339
- }
340
- }
341
- // Prefer Test Explorer structure (longer, more complete suite_title)
342
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
343
- existingTest.suite_title = test.suite_title;
344
- existingTest.file = this.extractCsFileFromPath(test);
345
- }
346
- }
347
- else {
348
- // Fix file path to use proper .cs file names from source paths
349
- test.file = this.extractCsFileFromPath(test);
350
- fqnMap.set(fqn, test);
351
- }
352
- });
353
- return Array.from(fqnMap.values());
354
- }
355
- generateFQN(test) {
356
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
357
- // Don't include assembly as it can vary between different test structures
358
- const namespace = this.extractNamespace(test);
359
- const className = this.extractClassName(test);
360
- const methodName = test.title;
361
- // Use the most complete namespace.class structure available
362
- if (test.suite_title && test.suite_title.includes('.')) {
363
- return `${test.suite_title}.${methodName}`;
364
- }
365
- return `${namespace}.${className}.${methodName}`;
366
- }
367
- generateNormalizedFQN(test) {
368
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
369
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
370
- const fullClassName = test.suite_title || '';
371
- const methodName = test.title;
372
- // Extract the most specific namespace.class pattern
373
- if (fullClassName.includes('.')) {
374
- const parts = fullClassName.split('.');
375
- if (parts.length >= 2) {
376
- const className = parts[parts.length - 1];
377
- // Look for common .NET namespace patterns and normalize them:
378
- // TestProject.Tests.MyClass -> Tests.MyClass
379
- // Tests.MyClass -> Tests.MyClass
380
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
381
- let normalizedNamespace = '';
382
- for (let i = parts.length - 2; i >= 0; i--) {
383
- const part = parts[i];
384
- // Build namespace from right to left, excluding project names
385
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
386
- // Found a test namespace, use it as the normalized namespace
387
- normalizedNamespace = part;
388
- break;
389
- }
390
- else if (i === parts.length - 2) {
391
- // If no test namespace found, use the immediate parent as namespace
392
- normalizedNamespace = part;
393
- }
394
- }
395
- return `${normalizedNamespace}.${className}.${methodName}`;
396
- }
397
- }
398
- // Fallback for simple class names
399
- return `${fullClassName}.${methodName}`;
400
- }
401
- extractAssemblyName(test) {
402
- // Extract assembly name from file path or use default
403
- if (test.file) {
404
- const parts = test.file.split(/[/\\]/);
405
- return parts[0] || 'DefaultAssembly';
406
- }
407
- return 'DefaultAssembly';
408
- }
409
- extractNamespace(test) {
410
- // Extract namespace from suite_title or classname
411
- if (test.suite_title && test.suite_title.includes('.')) {
412
- const parts = test.suite_title.split('.');
413
- return parts.slice(0, -1).join('.');
414
- }
415
- return test.suite_title || 'DefaultNamespace';
416
- }
417
- extractClassName(test) {
418
- // Extract class name from suite_title
419
- if (test.suite_title && test.suite_title.includes('.')) {
420
- const parts = test.suite_title.split('.');
421
- return parts[parts.length - 1];
422
- }
423
- return test.suite_title || 'DefaultClass';
424
- }
425
- extractCsFileFromPath(test) {
426
- // Extract .cs file name from source file path, not namespace
427
- if (test.file) {
428
- // Look for actual .cs file path patterns
429
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
430
- if (csFileMatch) {
431
- return test.file;
432
- }
433
- // If no .cs extension, assume it's a namespace path and convert to likely file name
434
- const className = this.extractClassName(test);
435
- const pathParts = test.file.split(/[/\\]/);
436
- pathParts[pathParts.length - 1] = `${className}.cs`;
437
- return pathParts.join('/');
438
- }
439
- // Fallback to class name
440
- const className = this.extractClassName(test);
441
- return `${className}.cs`;
442
- }
443
278
  calculateStats() {
444
279
  this.stats = {
445
280
  ...this.stats,
@@ -595,8 +430,7 @@ function reduceTestCases(prev, item) {
595
430
  testCases
596
431
  .filter(t => !!t)
597
432
  .forEach(testCaseItem => {
598
- // Use consistent Test Explorer structure: prioritize fullname for file path
599
- const file = extractSourceFilePath(testCaseItem, item);
433
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
600
434
  let stack = '';
601
435
  let message = '';
602
436
  if (testCaseItem.error)
@@ -616,20 +450,16 @@ function reduceTestCases(prev, item) {
616
450
  if (!message)
617
451
  message = stack.trim().split('\n')[0];
618
452
  const isParametrized = item.type === 'ParameterizedMethod';
453
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
619
454
  // SpecFlow config
620
455
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
621
456
  let example = null;
622
- // Use consistent Test Explorer structure for suite title
623
- const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
457
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
624
458
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
625
459
  tags ||= [];
626
- // Store original test name for parameter extraction
627
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
628
- const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
460
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
629
461
  if (exampleMatches) {
630
- // Extract and store parameters as Examples
631
- const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
632
- example = { ...parameterValues };
462
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
633
463
  title = title.replace(/\(.*?\)/, '').trim();
634
464
  }
635
465
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
@@ -678,7 +508,6 @@ function reduceTestCases(prev, item) {
678
508
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
679
509
  status,
680
510
  title,
681
- originalTestName, // Store original name for parameter-aware FQN generation
682
511
  root_suite_id: TESTOMATIO_SUITE,
683
512
  suite_title: suiteTitle,
684
513
  files,
@@ -687,51 +516,6 @@ function reduceTestCases(prev, item) {
687
516
  });
688
517
  return prev;
689
518
  }
690
- function extractSourceFilePath(testCaseItem, item) {
691
- // Priority order for file path extraction to match Test Explorer structure:
692
- // 1. fullname (contains full project path)
693
- // 2. filepath (direct file path)
694
- // 3. file attribute from test case
695
- // 4. package (fallback)
696
- if (item.fullname) {
697
- // Extract actual file path from fullname if it contains path separators
698
- const fullnameParts = item.fullname.split('.');
699
- if (fullnameParts.length > 2) {
700
- // Reconstruct path from project.namespace.class structure
701
- const projectName = fullnameParts[0];
702
- const namespaceParts = fullnameParts.slice(1, -1);
703
- const className = fullnameParts[fullnameParts.length - 1];
704
- return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
705
- }
706
- }
707
- if (item.filepath)
708
- return item.filepath;
709
- if (testCaseItem.file)
710
- return testCaseItem.file;
711
- if (item.package)
712
- return item.package;
713
- // Fallback: construct from classname
714
- if (testCaseItem.classname) {
715
- const parts = testCaseItem.classname.split('.');
716
- const className = parts[parts.length - 1];
717
- const namespacePath = parts.slice(0, -1).join('/');
718
- return `${namespacePath}/${className}.cs`;
719
- }
720
- return '';
721
- }
722
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
723
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
724
- // Priority: fullname > classname > name
725
- if (item.fullname) {
726
- // Use fullname to maintain Test Explorer structure
727
- return item.fullname;
728
- }
729
- if (testCaseItem.classname) {
730
- return testCaseItem.classname;
731
- }
732
- // Fallback to item name but prefer classname structure
733
- return item.name || testCaseItem.classname || 'UnknownClass';
734
- }
735
519
  function processTestSuite(testsuite) {
736
520
  if (!testsuite)
737
521
  return [];
@@ -743,14 +527,8 @@ function processTestSuite(testsuite) {
743
527
  if (!Array.isArray(testsuite)) {
744
528
  suites = [testsuite];
745
529
  }
746
- // Only process suites that have test cases OR child suites, but avoid double processing
747
- const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
748
- const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
749
- // Process child suites recursively
750
- const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
751
- // Process leaf suites with actual test cases
752
- const leafResults = leafSuites.reduce(reduceTestCases, []);
753
- return [...childResults, ...leafResults];
530
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
531
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
754
532
  }
755
533
  function fetchProperties(item) {
756
534
  const tags = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.1.3-beta.2-xml-import",
3
+ "version": "2.2.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -187,7 +187,8 @@ class TestomatioPipe {
187
187
  const resp = await this.client.request({
188
188
  method: 'PUT',
189
189
  url: `/api/reporter/${this.runId}`,
190
- data: runParams
190
+ data: runParams,
191
+ responseType: 'json'
191
192
  });
192
193
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
193
194
  return;
package/src/xmlReader.js CHANGED
@@ -161,11 +161,8 @@ class XmlReader {
161
161
 
162
162
  reduceOptions.preferClassname = this.stats.language === 'python';
163
163
  const resultTests = processTestSuite(jsonSuite['test-suite']);
164
-
165
- // Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
166
- const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
167
164
 
168
- this.tests = this.tests.concat(deduplicatedTests);
165
+ this.tests = this.tests.concat(resultTests);
169
166
 
170
167
  return {
171
168
  status: result?.toLowerCase(),
@@ -174,7 +171,7 @@ class XmlReader {
174
171
  passed_count: parseInt(passed, 10),
175
172
  failed_count: parseInt(failed, 10),
176
173
  skipped_count: parseInt(inconclusive + skipped, 10),
177
- tests: deduplicatedTests,
174
+ tests: resultTests,
178
175
  };
179
176
  }
180
177
 
@@ -322,194 +319,6 @@ class XmlReader {
322
319
  };
323
320
  }
324
321
 
325
- deduplicateTestsByFQN(tests) {
326
- const fqnMap = new Map();
327
-
328
- tests.forEach(test => {
329
- const fqn = this.generateNormalizedFQN(test);
330
-
331
- if (fqnMap.has(fqn)) {
332
- const existingTest = fqnMap.get(fqn);
333
-
334
- // For parameterized tests, merge as Examples
335
- if (test.example) {
336
- // Initialize examples array if it doesn't exist
337
- if (!existingTest.examples) {
338
- existingTest.examples = [];
339
- // Add the existing test's example as the first item
340
- if (existingTest.example) {
341
- existingTest.examples.push({
342
- parameters: existingTest.example,
343
- status: existingTest.status,
344
- run_time: existingTest.run_time,
345
- message: existingTest.message,
346
- stack: existingTest.stack
347
- });
348
- }
349
- }
350
-
351
- // Add this test's execution as an example
352
- existingTest.examples.push({
353
- parameters: test.example,
354
- status: test.status,
355
- run_time: test.run_time,
356
- message: test.message,
357
- stack: test.stack
358
- });
359
-
360
- // Update the main test status to reflect the worst status
361
- if (test.status === 'failed' || existingTest.status === 'failed') {
362
- existingTest.status = 'failed';
363
- } else if (test.status === 'skipped' && existingTest.status !== 'failed') {
364
- existingTest.status = 'skipped';
365
- }
366
-
367
- // Update total run time
368
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
369
-
370
- // Merge stack traces if they're different
371
- if (test.stack && test.stack !== existingTest.stack) {
372
- existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
373
- }
374
-
375
- // Merge messages if they're different
376
- if (test.message && test.message !== existingTest.message) {
377
- existingTest.message = existingTest.message + '; ' + test.message;
378
- }
379
- } else {
380
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
381
- if (test.test_id && !existingTest.test_id) {
382
- existingTest.test_id = test.test_id;
383
- }
384
- // Keep the most complete test data
385
- if (test.stack && !existingTest.stack) {
386
- existingTest.stack = test.stack;
387
- }
388
- if (test.message && !existingTest.message) {
389
- existingTest.message = test.message;
390
- }
391
- }
392
-
393
- // Prefer Test Explorer structure (longer, more complete suite_title)
394
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
395
- existingTest.suite_title = test.suite_title;
396
- existingTest.file = this.extractCsFileFromPath(test);
397
- }
398
- } else {
399
- // Fix file path to use proper .cs file names from source paths
400
- test.file = this.extractCsFileFromPath(test);
401
- fqnMap.set(fqn, test);
402
- }
403
- });
404
-
405
- return Array.from(fqnMap.values());
406
- }
407
-
408
- generateFQN(test) {
409
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
410
- // Don't include assembly as it can vary between different test structures
411
- const namespace = this.extractNamespace(test);
412
- const className = this.extractClassName(test);
413
- const methodName = test.title;
414
-
415
- // Use the most complete namespace.class structure available
416
- if (test.suite_title && test.suite_title.includes('.')) {
417
- return `${test.suite_title}.${methodName}`;
418
- }
419
-
420
- return `${namespace}.${className}.${methodName}`;
421
- }
422
-
423
- generateNormalizedFQN(test) {
424
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
425
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
426
-
427
- const fullClassName = test.suite_title || '';
428
- const methodName = test.title;
429
-
430
- // Extract the most specific namespace.class pattern
431
- if (fullClassName.includes('.')) {
432
- const parts = fullClassName.split('.');
433
-
434
- if (parts.length >= 2) {
435
- const className = parts[parts.length - 1];
436
-
437
- // Look for common .NET namespace patterns and normalize them:
438
- // TestProject.Tests.MyClass -> Tests.MyClass
439
- // Tests.MyClass -> Tests.MyClass
440
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
441
-
442
- let normalizedNamespace = '';
443
- for (let i = parts.length - 2; i >= 0; i--) {
444
- const part = parts[i];
445
-
446
- // Build namespace from right to left, excluding project names
447
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
448
- // Found a test namespace, use it as the normalized namespace
449
- normalizedNamespace = part;
450
- break;
451
- } else if (i === parts.length - 2) {
452
- // If no test namespace found, use the immediate parent as namespace
453
- normalizedNamespace = part;
454
- }
455
- }
456
-
457
- return `${normalizedNamespace}.${className}.${methodName}`;
458
- }
459
- }
460
-
461
- // Fallback for simple class names
462
- return `${fullClassName}.${methodName}`;
463
- }
464
-
465
- extractAssemblyName(test) {
466
- // Extract assembly name from file path or use default
467
- if (test.file) {
468
- const parts = test.file.split(/[/\\]/);
469
- return parts[0] || 'DefaultAssembly';
470
- }
471
- return 'DefaultAssembly';
472
- }
473
-
474
- extractNamespace(test) {
475
- // Extract namespace from suite_title or classname
476
- if (test.suite_title && test.suite_title.includes('.')) {
477
- const parts = test.suite_title.split('.');
478
- return parts.slice(0, -1).join('.');
479
- }
480
- return test.suite_title || 'DefaultNamespace';
481
- }
482
-
483
- extractClassName(test) {
484
- // Extract class name from suite_title
485
- if (test.suite_title && test.suite_title.includes('.')) {
486
- const parts = test.suite_title.split('.');
487
- return parts[parts.length - 1];
488
- }
489
- return test.suite_title || 'DefaultClass';
490
- }
491
-
492
- extractCsFileFromPath(test) {
493
- // Extract .cs file name from source file path, not namespace
494
- if (test.file) {
495
- // Look for actual .cs file path patterns
496
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
497
- if (csFileMatch) {
498
- return test.file;
499
- }
500
-
501
- // If no .cs extension, assume it's a namespace path and convert to likely file name
502
- const className = this.extractClassName(test);
503
- const pathParts = test.file.split(/[/\\]/);
504
- pathParts[pathParts.length - 1] = `${className}.cs`;
505
- return pathParts.join('/');
506
- }
507
-
508
- // Fallback to class name
509
- const className = this.extractClassName(test);
510
- return `${className}.cs`;
511
- }
512
-
513
322
  calculateStats() {
514
323
  this.stats = {
515
324
  ...this.stats,
@@ -676,8 +485,7 @@ function reduceTestCases(prev, item) {
676
485
  testCases
677
486
  .filter(t => !!t)
678
487
  .forEach(testCaseItem => {
679
- // Use consistent Test Explorer structure: prioritize fullname for file path
680
- const file = extractSourceFilePath(testCaseItem, item);
488
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
681
489
 
682
490
  let stack = '';
683
491
  let message = '';
@@ -692,25 +500,19 @@ function reduceTestCases(prev, item) {
692
500
  if (!message) message = stack.trim().split('\n')[0];
693
501
 
694
502
  const isParametrized = item.type === 'ParameterizedMethod';
503
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
695
504
 
696
505
  // SpecFlow config
697
506
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
698
507
  let example = null;
699
-
700
- // Use consistent Test Explorer structure for suite title
701
- const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
508
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
702
509
 
703
510
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
704
511
  tags ||= [];
705
512
 
706
- // Store original test name for parameter extraction
707
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
708
-
709
- const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
513
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
710
514
  if (exampleMatches) {
711
- // Extract and store parameters as Examples
712
- const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
713
- example = { ...parameterValues };
515
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
714
516
  title = title.replace(/\(.*?\)/, '').trim();
715
517
  }
716
518
 
@@ -766,7 +568,6 @@ function reduceTestCases(prev, item) {
766
568
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
767
569
  status,
768
570
  title,
769
- originalTestName, // Store original name for parameter-aware FQN generation
770
571
  root_suite_id: TESTOMATIO_SUITE,
771
572
  suite_title: suiteTitle,
772
573
  files,
@@ -776,57 +577,6 @@ function reduceTestCases(prev, item) {
776
577
  return prev;
777
578
  }
778
579
 
779
- function extractSourceFilePath(testCaseItem, item) {
780
- // Priority order for file path extraction to match Test Explorer structure:
781
- // 1. fullname (contains full project path)
782
- // 2. filepath (direct file path)
783
- // 3. file attribute from test case
784
- // 4. package (fallback)
785
-
786
- if (item.fullname) {
787
- // Extract actual file path from fullname if it contains path separators
788
- const fullnameParts = item.fullname.split('.');
789
- if (fullnameParts.length > 2) {
790
- // Reconstruct path from project.namespace.class structure
791
- const projectName = fullnameParts[0];
792
- const namespaceParts = fullnameParts.slice(1, -1);
793
- const className = fullnameParts[fullnameParts.length - 1];
794
- return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
795
- }
796
- }
797
-
798
- if (item.filepath) return item.filepath;
799
- if (testCaseItem.file) return testCaseItem.file;
800
- if (item.package) return item.package;
801
-
802
- // Fallback: construct from classname
803
- if (testCaseItem.classname) {
804
- const parts = testCaseItem.classname.split('.');
805
- const className = parts[parts.length - 1];
806
- const namespacePath = parts.slice(0, -1).join('/');
807
- return `${namespacePath}/${className}.cs`;
808
- }
809
-
810
- return '';
811
- }
812
-
813
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
814
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
815
- // Priority: fullname > classname > name
816
-
817
- if (item.fullname) {
818
- // Use fullname to maintain Test Explorer structure
819
- return item.fullname;
820
- }
821
-
822
- if (testCaseItem.classname) {
823
- return testCaseItem.classname;
824
- }
825
-
826
- // Fallback to item name but prefer classname structure
827
- return item.name || testCaseItem.classname || 'UnknownClass';
828
- }
829
-
830
580
  function processTestSuite(testsuite) {
831
581
  if (!testsuite) return [];
832
582
  if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
@@ -837,17 +587,9 @@ function processTestSuite(testsuite) {
837
587
  suites = [testsuite];
838
588
  }
839
589
 
840
- // Only process suites that have test cases OR child suites, but avoid double processing
841
- const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
842
- const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
843
-
844
- // Process child suites recursively
845
- const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
846
-
847
- // Process leaf suites with actual test cases
848
- const leafResults = leafSuites.reduce(reduceTestCases, []);
590
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
849
591
 
850
- return [...childResults, ...leafResults];
592
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
851
593
  }
852
594
 
853
595
  function fetchProperties(item) {