@testomatio/reporter 2.1.3-beta.1-multi-links → 2.1.3-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/src/xmlReader.js CHANGED
@@ -161,8 +161,11 @@ 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);
164
167
 
165
- this.tests = this.tests.concat(resultTests);
168
+ this.tests = this.tests.concat(deduplicatedTests);
166
169
 
167
170
  return {
168
171
  status: result?.toLowerCase(),
@@ -171,7 +174,7 @@ class XmlReader {
171
174
  passed_count: parseInt(passed, 10),
172
175
  failed_count: parseInt(failed, 10),
173
176
  skipped_count: parseInt(inconclusive + skipped, 10),
174
- tests: resultTests,
177
+ tests: deduplicatedTests,
175
178
  };
176
179
  }
177
180
 
@@ -319,6 +322,194 @@ class XmlReader {
319
322
  };
320
323
  }
321
324
 
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
+
322
513
  calculateStats() {
323
514
  this.stats = {
324
515
  ...this.stats,
@@ -485,7 +676,8 @@ function reduceTestCases(prev, item) {
485
676
  testCases
486
677
  .filter(t => !!t)
487
678
  .forEach(testCaseItem => {
488
- const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
679
+ // Use consistent Test Explorer structure: prioritize fullname for file path
680
+ const file = extractSourceFilePath(testCaseItem, item);
489
681
 
490
682
  let stack = '';
491
683
  let message = '';
@@ -500,19 +692,25 @@ function reduceTestCases(prev, item) {
500
692
  if (!message) message = stack.trim().split('\n')[0];
501
693
 
502
694
  const isParametrized = item.type === 'ParameterizedMethod';
503
- const preferClassname = reduceOptions.preferClassname || isParametrized;
504
695
 
505
696
  // SpecFlow config
506
697
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
507
698
  let example = null;
508
- const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
699
+
700
+ // Use consistent Test Explorer structure for suite title
701
+ const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
509
702
 
510
703
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
511
704
  tags ||= [];
512
705
 
513
- const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
706
+ // Store original test name for parameter extraction
707
+ const originalTestName = testCaseItem.name || testCaseItem.methodname;
708
+
709
+ const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
514
710
  if (exampleMatches) {
515
- example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
711
+ // Extract and store parameters as Examples
712
+ const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
713
+ example = { ...parameterValues };
516
714
  title = title.replace(/\(.*?\)/, '').trim();
517
715
  }
518
716
 
@@ -568,6 +766,7 @@ function reduceTestCases(prev, item) {
568
766
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
569
767
  status,
570
768
  title,
769
+ originalTestName, // Store original name for parameter-aware FQN generation
571
770
  root_suite_id: TESTOMATIO_SUITE,
572
771
  suite_title: suiteTitle,
573
772
  files,
@@ -577,6 +776,57 @@ function reduceTestCases(prev, item) {
577
776
  return prev;
578
777
  }
579
778
 
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
+
580
830
  function processTestSuite(testsuite) {
581
831
  if (!testsuite) return [];
582
832
  if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
@@ -587,9 +837,17 @@ function processTestSuite(testsuite) {
587
837
  suites = [testsuite];
588
838
  }
589
839
 
590
- const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
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, []);
591
849
 
592
- return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
850
+ return [...childResults, ...leafResults];
593
851
  }
594
852
 
595
853
  function fetchProperties(item) {
@@ -1 +0,0 @@
1
- export function checkForEnvPassedAsArguments(): void;