@testomatio/reporter 2.3.5-beta-5-xml-import → 2.3.5-beta.8-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
@@ -162,29 +162,7 @@ class XmlReader {
162
162
  reduceOptions.preferClassname = this.stats.language === 'python';
163
163
  const resultTests = processTestSuite(jsonSuite['test-suite']);
164
164
 
165
- debug('Raw tests extracted from NUnit XML:', resultTests.length);
166
- debug(
167
- 'Raw tests:',
168
- resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })),
169
- );
170
-
171
- // Optional deduplication for complex NUnit scenarios - can be enabled via options
172
- let finalTests = resultTests;
173
- if (this.opts.enableNUnitDeduplication) {
174
- finalTests = this.deduplicateTestsByFQN(resultTests);
175
- debug('Tests after deduplication:', finalTests.length);
176
- debug(
177
- 'Deduplicated tests:',
178
- finalTests.map(t => ({
179
- title: t.title,
180
- examples: t.examples,
181
- example: t.example,
182
- file: t.file,
183
- })),
184
- );
185
- }
186
-
187
- this.tests = this.tests.concat(finalTests);
165
+ this.tests = this.tests.concat(resultTests);
188
166
 
189
167
  return {
190
168
  status: result?.toLowerCase(),
@@ -193,7 +171,7 @@ class XmlReader {
193
171
  passed_count: parseInt(passed, 10),
194
172
  failed_count: parseInt(failed, 10),
195
173
  skipped_count: parseInt(inconclusive + skipped, 10),
196
- tests: finalTests,
174
+ tests: resultTests,
197
175
  };
198
176
  }
199
177
 
@@ -341,194 +319,6 @@ class XmlReader {
341
319
  };
342
320
  }
343
321
 
344
- deduplicateTestsByFQN(tests) {
345
- const fqnMap = new Map();
346
-
347
- tests.forEach(test => {
348
- const fqn = this.generateNormalizedFQN(test);
349
-
350
- if (fqnMap.has(fqn)) {
351
- const existingTest = fqnMap.get(fqn);
352
-
353
- // For parameterized tests, merge as Examples
354
- if (test.example && Array.isArray(test.example) && test.example.length > 0) {
355
- // Initialize examples array if it doesn't exist
356
- if (!existingTest.examples) {
357
- existingTest.examples = [];
358
- // Add the existing test's example as the first item if it has parameters
359
- if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
360
- existingTest.examples.push({
361
- parameters: existingTest.example,
362
- status: existingTest.status,
363
- run_time: existingTest.run_time,
364
- message: existingTest.message,
365
- stack: existingTest.stack,
366
- });
367
- // Clear the main test's example since it's now in examples array
368
- delete existingTest.example;
369
- }
370
- }
371
-
372
- // Add this test's execution as an example
373
- existingTest.examples.push({
374
- parameters: test.example,
375
- status: test.status,
376
- run_time: test.run_time,
377
- message: test.message,
378
- stack: test.stack,
379
- });
380
-
381
- // Update the main test status to reflect the worst status
382
- if (test.status === 'failed' || existingTest.status === 'failed') {
383
- existingTest.status = 'failed';
384
- } else if (test.status === 'skipped' && existingTest.status !== 'failed') {
385
- existingTest.status = 'skipped';
386
- }
387
-
388
- // Update total run time
389
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
390
- } else {
391
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
392
- if (test.test_id && !existingTest.test_id) {
393
- existingTest.test_id = test.test_id;
394
- }
395
- // Keep the most complete test data
396
- if (test.stack && !existingTest.stack) {
397
- existingTest.stack = test.stack;
398
- }
399
- if (test.message && !existingTest.message) {
400
- existingTest.message = test.message;
401
- }
402
- }
403
-
404
- // Prefer Test Explorer structure (longer, more complete suite_title)
405
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
406
- existingTest.suite_title = test.suite_title;
407
- }
408
-
409
- // Always use the source file path if available
410
- if (test.file && test.file.endsWith('.cs')) {
411
- existingTest.file = test.file;
412
- } else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
413
- existingTest.file = this.extractCsFileFromPath(test);
414
- }
415
- } else {
416
- // Fix file path to use proper .cs file names from source paths
417
- if (!test.file || !test.file.endsWith('.cs')) {
418
- test.file = this.extractCsFileFromPath(test);
419
- }
420
- fqnMap.set(fqn, test);
421
- }
422
- });
423
-
424
- return Array.from(fqnMap.values());
425
- }
426
-
427
- generateFQN(test) {
428
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
429
- // Don't include assembly as it can vary between different test structures
430
- const namespace = this.extractNamespace(test);
431
- const className = this.extractClassName(test);
432
- const methodName = test.title;
433
-
434
- // Use the most complete namespace.class structure available
435
- if (test.suite_title && test.suite_title.includes('.')) {
436
- return `${test.suite_title}.${methodName}`;
437
- }
438
-
439
- return `${namespace}.${className}.${methodName}`;
440
- }
441
-
442
- generateNormalizedFQN(test) {
443
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
444
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
445
-
446
- const fullClassName = test.suite_title || '';
447
- const methodName = test.title;
448
-
449
- // Extract the most specific namespace.class pattern
450
- if (fullClassName.includes('.')) {
451
- const parts = fullClassName.split('.');
452
-
453
- if (parts.length >= 2) {
454
- const className = parts[parts.length - 1];
455
-
456
- // Look for common .NET namespace patterns and normalize them:
457
- // TestProject.Tests.MyClass -> Tests.MyClass
458
- // Tests.MyClass -> Tests.MyClass
459
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
460
-
461
- let normalizedNamespace = '';
462
- for (let i = parts.length - 2; i >= 0; i--) {
463
- const part = parts[i];
464
-
465
- // Build namespace from right to left, excluding project names
466
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
467
- // Found a test namespace, use it as the normalized namespace
468
- normalizedNamespace = part;
469
- break;
470
- } else if (i === parts.length - 2) {
471
- // If no test namespace found, use the immediate parent as namespace
472
- normalizedNamespace = part;
473
- }
474
- }
475
-
476
- return `${normalizedNamespace}.${className}.${methodName}`;
477
- }
478
- }
479
-
480
- // Fallback for simple class names
481
- return `${fullClassName}.${methodName}`;
482
- }
483
-
484
- extractAssemblyName(test) {
485
- // Extract assembly name from file path or use default
486
- if (test.file) {
487
- const parts = test.file.split(/[/\\]/);
488
- return parts[0] || 'DefaultAssembly';
489
- }
490
- return 'DefaultAssembly';
491
- }
492
-
493
- extractNamespace(test) {
494
- // Extract namespace from suite_title or classname
495
- if (test.suite_title && test.suite_title.includes('.')) {
496
- const parts = test.suite_title.split('.');
497
- return parts.slice(0, -1).join('.');
498
- }
499
- return test.suite_title || 'DefaultNamespace';
500
- }
501
-
502
- extractClassName(test) {
503
- // Extract class name from suite_title
504
- if (test.suite_title && test.suite_title.includes('.')) {
505
- const parts = test.suite_title.split('.');
506
- return parts[parts.length - 1];
507
- }
508
- return test.suite_title || 'DefaultClass';
509
- }
510
-
511
- extractCsFileFromPath(test) {
512
- // Extract .cs file name from source file path, not namespace
513
- if (test.file) {
514
- // Look for actual .cs file path patterns
515
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
516
- if (csFileMatch) {
517
- return test.file;
518
- }
519
-
520
- // If no .cs extension, assume it's a namespace path and convert to likely file name
521
- const className = this.extractClassName(test);
522
- const pathParts = test.file.split(/[/\\]/);
523
- pathParts[pathParts.length - 1] = `${className}.cs`;
524
- return pathParts.join('/');
525
- }
526
-
527
- // Fallback to class name
528
- const className = this.extractClassName(test);
529
- return `${className}.cs`;
530
- }
531
-
532
322
  calculateStats() {
533
323
  this.stats = {
534
324
  ...this.stats,
@@ -570,15 +360,9 @@ class XmlReader {
570
360
  debug('Failed to open file with the source code', file);
571
361
  return;
572
362
  }
573
-
574
363
  const contents = fs.readFileSync(file).toString();
575
-
576
- // Try original test name first (for parameterized tests), fallback to regular title
577
- const titleForLookup = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
578
-
579
- t.code = fetchSourceCode(contents, { ...t, title: titleForLookup, lang: this.stats.language });
364
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language });
580
365
  if (t.code) debug('Fetched code for test %s', t.title);
581
-
582
366
  t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language });
583
367
  if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
584
368
  } catch (err) {
@@ -701,13 +485,7 @@ function reduceTestCases(prev, item) {
701
485
  testCases
702
486
  .filter(t => !!t)
703
487
  .forEach(testCaseItem => {
704
- // Simple file extraction (version 2.1.1 approach) with fallback to enhanced extraction
705
- let file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
706
-
707
- // If no file found with simple approach and we have enhanced extraction enabled, use it
708
- if (!file && item.filepath) {
709
- file = extractSourceFilePath(testCaseItem, item);
710
- }
488
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
711
489
 
712
490
  let stack = '';
713
491
  let message = '';
@@ -727,34 +505,15 @@ function reduceTestCases(prev, item) {
727
505
  // SpecFlow config
728
506
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
729
507
  let example = null;
730
-
731
- // Simple suite title extraction (version 2.1.1 approach) with fallback to enhanced
732
- let suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
733
- if (!suiteTitle && item.fullname) {
734
- suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
735
- }
508
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
736
509
 
737
510
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
738
511
  tags ||= [];
739
512
 
740
- // Store original test name for enhanced parameter extraction
741
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
742
-
743
- // Enhanced NUnit-style arguments from <arguments> element
744
- if (testCaseItem.arguments && testCaseItem.arguments.arg) {
745
- const args = Array.isArray(testCaseItem.arguments.arg)
746
- ? testCaseItem.arguments.arg
747
- : [testCaseItem.arguments.arg];
748
- example = args; // Store as array instead of object
749
- // Remove parameters from title for NUnit tests
750
- title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
751
- } else {
752
- // Simple parameter extraction (version 2.1.1 approach)
753
- const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
754
- if (exampleMatches) {
755
- example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
756
- title = title.replace(/\(.*?\)/, '').trim();
757
- }
513
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
514
+ if (exampleMatches) {
515
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
516
+ title = title.replace(/\(.*?\)/, '').trim();
758
517
  }
759
518
 
760
519
  stack = `${
@@ -809,7 +568,6 @@ function reduceTestCases(prev, item) {
809
568
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
810
569
  status,
811
570
  title,
812
- originalTestName, // Store original name for enhanced features
813
571
  root_suite_id: TESTOMATIO_SUITE,
814
572
  suite_title: suiteTitle,
815
573
  files,
@@ -819,113 +577,6 @@ function reduceTestCases(prev, item) {
819
577
  return prev;
820
578
  }
821
579
 
822
- function extractSourceFilePath(testCaseItem, item) {
823
- // Priority order for file path extraction to match Test Explorer structure:
824
- // 1. filepath attribute (direct .cs file path from NUnit)
825
- // 2. fullname (contains full project path)
826
- // 3. file attribute from test case
827
- // 4. package (fallback)
828
-
829
- // NUnit provides filepath attribute with actual .cs file path - use this first
830
- if (item.filepath) {
831
- // Clean up Windows/Unix path separators and ensure proper format
832
- let filePath = item.filepath.replace(/\\/g, '/');
833
-
834
- // Make relative to current working directory if absolute
835
- if (path.isAbsolute(item.filepath)) {
836
- const cwd = process.cwd().replace(/\\/g, '/');
837
- if (filePath.startsWith(cwd)) {
838
- filePath = path.relative(cwd, item.filepath).replace(/\\/g, '/');
839
- } else {
840
- // Try to extract relative path from common patterns
841
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
842
- for (const pattern of commonPatterns) {
843
- const index = filePath.lastIndexOf(pattern);
844
- if (index !== -1) {
845
- filePath = filePath.substring(index + 1);
846
- break;
847
- }
848
- }
849
- }
850
- }
851
- return filePath;
852
- }
853
-
854
- if (testCaseItem.file) {
855
- let filePath = testCaseItem.file.replace(/\\/g, '/');
856
-
857
- // Make relative to current working directory if absolute
858
- if (path.isAbsolute(testCaseItem.file)) {
859
- const cwd = process.cwd().replace(/\\/g, '/');
860
- if (filePath.startsWith(cwd)) {
861
- filePath = path.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
862
- } else {
863
- // Try to extract relative path from common patterns
864
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
865
- for (const pattern of commonPatterns) {
866
- const index = filePath.lastIndexOf(pattern);
867
- if (index !== -1) {
868
- filePath = filePath.substring(index + 1);
869
- break;
870
- }
871
- }
872
- }
873
- }
874
- return filePath;
875
- }
876
-
877
- if (item.fullname) {
878
- // Extract actual file path from fullname if it contains path separators
879
- const fullnameParts = item.fullname.split('.');
880
- if (fullnameParts.length > 2) {
881
- // For ParameterizedMethod, get the class name (not method name)
882
- // Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
883
- let namespaceParts, className;
884
-
885
- if (item.type === 'ParameterizedMethod') {
886
- // For parameterized methods, the last part is the method name, second-to-last is class
887
- namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
888
- className = fullnameParts[fullnameParts.length - 2]; // Get class name
889
- } else {
890
- // For regular classes/fixtures
891
- namespaceParts = fullnameParts.slice(1, -1); // Skip project name
892
- className = fullnameParts[fullnameParts.length - 1];
893
- }
894
-
895
- return `${namespaceParts.join('/')}/${className}.cs`;
896
- }
897
- }
898
-
899
- if (item.package) return item.package.replace(/\\/g, '/');
900
-
901
- // Fallback: construct from classname
902
- if (testCaseItem.classname) {
903
- const parts = testCaseItem.classname.split('.');
904
- const className = parts[parts.length - 1];
905
- const namespacePath = parts.slice(0, -1).join('/');
906
- return `${namespacePath}/${className}.cs`;
907
- }
908
-
909
- return '';
910
- }
911
-
912
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
913
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
914
- // Priority: fullname > classname > name
915
-
916
- if (item.fullname) {
917
- // Use fullname to maintain Test Explorer structure
918
- return item.fullname;
919
- }
920
-
921
- if (testCaseItem.classname) {
922
- return testCaseItem.classname;
923
- }
924
-
925
- // Fallback to item name but prefer classname structure
926
- return item.name || testCaseItem.classname || 'UnknownClass';
927
- }
928
-
929
580
  function processTestSuite(testsuite) {
930
581
  if (!testsuite) return [];
931
582
  if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
@@ -936,8 +587,7 @@ function processTestSuite(testsuite) {
936
587
  suites = [testsuite];
937
588
  }
938
589
 
939
- // Simple approach from version 2.1.1 with enhanced processing for complex scenarios
940
- const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
590
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
941
591
 
942
592
  return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
943
593
  }