@testomatio/reporter 2.3.4 → 2.3.5-beta-6-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/xmlReader.js CHANGED
@@ -20,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
20
20
  const debug = (0, debug_1.default)('@testomatio/reporter:xml');
21
21
  const ridRunId = (0, crypto_1.randomUUID)();
22
22
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
23
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
23
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, TESTOMATIO_DISABLE_SOURCE_CODE, } = process.env;
24
24
  const options = {
25
25
  ignoreDeclaration: true,
26
26
  ignoreAttributes: false,
@@ -47,6 +47,10 @@ class XmlReader {
47
47
  if (!this.adapter)
48
48
  throw new Error('XML adapter for this format not found');
49
49
  this.opts = opts || {};
50
+ // Check if source code fetching should be disabled
51
+ this.disableSourceCodeFetching = opts.disableSourceCodeFetching || TESTOMATIO_DISABLE_SOURCE_CODE;
52
+ // Control suite organization strategy: 'classname' (default) or 'fullpath'
53
+ this.suiteOrganization = opts.suiteOrganization || process.env.TESTOMATIO_SUITE_ORGANIZATION || 'classname';
50
54
  this.store = {};
51
55
  this.pipesPromise = (0, index_js_1.pipesFactory)(opts, this.store);
52
56
  this.parser = new fast_xml_parser_1.XMLParser(options);
@@ -58,6 +62,15 @@ class XmlReader {
58
62
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
59
63
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
60
64
  console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
65
+ if (this.disableSourceCodeFetching) {
66
+ console.log(constants_js_1.APP_PREFIX, '🚫 Source code fetching is disabled');
67
+ }
68
+ if (this.suiteOrganization === 'fullpath') {
69
+ console.log(constants_js_1.APP_PREFIX, '📁 Using fullpath suite organization (may create nested structure)');
70
+ }
71
+ else {
72
+ console.log(constants_js_1.APP_PREFIX, '📋 Using classname suite organization (avoids duplicates)');
73
+ }
61
74
  }
62
75
  connectAdapter() {
63
76
  if (this.opts.javaTests) {
@@ -130,8 +143,23 @@ class XmlReader {
130
143
  processNUnit(jsonSuite) {
131
144
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
145
  reduceOptions.preferClassname = this.stats.language === 'python';
146
+ reduceOptions.suiteOrganization = this.suiteOrganization;
133
147
  const resultTests = processTestSuite(jsonSuite['test-suite']);
134
- this.tests = this.tests.concat(resultTests);
148
+ debug('Raw tests extracted from NUnit XML:', resultTests.length);
149
+ debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
150
+ // Optional deduplication for complex NUnit scenarios - can be enabled via options
151
+ let finalTests = resultTests;
152
+ if (this.opts.enableNUnitDeduplication) {
153
+ finalTests = this.deduplicateTestsByFQN(resultTests);
154
+ debug('Tests after deduplication:', finalTests.length);
155
+ debug('Deduplicated tests:', finalTests.map(t => ({
156
+ title: t.title,
157
+ examples: t.examples,
158
+ example: t.example,
159
+ file: t.file,
160
+ })));
161
+ }
162
+ this.tests = this.tests.concat(finalTests);
135
163
  return {
136
164
  status: result?.toLowerCase(),
137
165
  create_tests: true,
@@ -139,7 +167,7 @@ class XmlReader {
139
167
  passed_count: parseInt(passed, 10),
140
168
  failed_count: parseInt(failed, 10),
141
169
  skipped_count: parseInt(inconclusive + skipped, 10),
142
- tests: resultTests,
170
+ tests: finalTests,
143
171
  };
144
172
  }
145
173
  processTRX(jsonSuite) {
@@ -275,6 +303,171 @@ class XmlReader {
275
303
  tests,
276
304
  };
277
305
  }
306
+ deduplicateTestsByFQN(tests) {
307
+ const fqnMap = new Map();
308
+ tests.forEach(test => {
309
+ const fqn = this.generateNormalizedFQN(test);
310
+ if (fqnMap.has(fqn)) {
311
+ const existingTest = fqnMap.get(fqn);
312
+ // For parameterized tests, merge as Examples
313
+ if (test.example && Array.isArray(test.example) && test.example.length > 0) {
314
+ // Initialize examples array if it doesn't exist
315
+ if (!existingTest.examples) {
316
+ existingTest.examples = [];
317
+ // Add the existing test's example as the first item if it has parameters
318
+ if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
319
+ existingTest.examples.push({
320
+ parameters: existingTest.example,
321
+ status: existingTest.status,
322
+ run_time: existingTest.run_time,
323
+ message: existingTest.message,
324
+ stack: existingTest.stack,
325
+ });
326
+ // Clear the main test's example since it's now in examples array
327
+ delete existingTest.example;
328
+ }
329
+ }
330
+ // Add this test's execution as an example
331
+ existingTest.examples.push({
332
+ parameters: test.example,
333
+ status: test.status,
334
+ run_time: test.run_time,
335
+ message: test.message,
336
+ stack: test.stack,
337
+ });
338
+ // Update the main test status to reflect the worst status
339
+ if (test.status === 'failed' || existingTest.status === 'failed') {
340
+ existingTest.status = 'failed';
341
+ }
342
+ else if (test.status === 'skipped' && existingTest.status !== 'failed') {
343
+ existingTest.status = 'skipped';
344
+ }
345
+ // Update total run time
346
+ existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
347
+ }
348
+ else {
349
+ // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
350
+ if (test.test_id && !existingTest.test_id) {
351
+ existingTest.test_id = test.test_id;
352
+ }
353
+ // Keep the most complete test data
354
+ if (test.stack && !existingTest.stack) {
355
+ existingTest.stack = test.stack;
356
+ }
357
+ if (test.message && !existingTest.message) {
358
+ existingTest.message = test.message;
359
+ }
360
+ }
361
+ // Prefer Test Explorer structure (longer, more complete suite_title)
362
+ if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
363
+ existingTest.suite_title = test.suite_title;
364
+ }
365
+ // Always use the source file path if available
366
+ if (test.file && test.file.endsWith('.cs')) {
367
+ existingTest.file = test.file;
368
+ }
369
+ else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
370
+ existingTest.file = this.extractCsFileFromPath(test);
371
+ }
372
+ }
373
+ else {
374
+ // Fix file path to use proper .cs file names from source paths
375
+ if (!test.file || !test.file.endsWith('.cs')) {
376
+ test.file = this.extractCsFileFromPath(test);
377
+ }
378
+ fqnMap.set(fqn, test);
379
+ }
380
+ });
381
+ return Array.from(fqnMap.values());
382
+ }
383
+ generateFQN(test) {
384
+ // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
385
+ // Don't include assembly as it can vary between different test structures
386
+ const namespace = this.extractNamespace(test);
387
+ const className = this.extractClassName(test);
388
+ const methodName = test.title;
389
+ // Use the most complete namespace.class structure available
390
+ if (test.suite_title && test.suite_title.includes('.')) {
391
+ return `${test.suite_title}.${methodName}`;
392
+ }
393
+ return `${namespace}.${className}.${methodName}`;
394
+ }
395
+ generateNormalizedFQN(test) {
396
+ // Generate normalized FQN for deduplication by extracting the core namespace.class.method
397
+ // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
398
+ const fullClassName = test.suite_title || '';
399
+ const methodName = test.title;
400
+ // Extract the most specific namespace.class pattern
401
+ if (fullClassName.includes('.')) {
402
+ const parts = fullClassName.split('.');
403
+ if (parts.length >= 2) {
404
+ const className = parts[parts.length - 1];
405
+ // Look for common .NET namespace patterns and normalize them:
406
+ // TestProject.Tests.MyClass -> Tests.MyClass
407
+ // Tests.MyClass -> Tests.MyClass
408
+ // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
409
+ let normalizedNamespace = '';
410
+ for (let i = parts.length - 2; i >= 0; i--) {
411
+ const part = parts[i];
412
+ // Build namespace from right to left, excluding project names
413
+ if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
414
+ // Found a test namespace, use it as the normalized namespace
415
+ normalizedNamespace = part;
416
+ break;
417
+ }
418
+ else if (i === parts.length - 2) {
419
+ // If no test namespace found, use the immediate parent as namespace
420
+ normalizedNamespace = part;
421
+ }
422
+ }
423
+ return `${normalizedNamespace}.${className}.${methodName}`;
424
+ }
425
+ }
426
+ // Fallback for simple class names
427
+ return `${fullClassName}.${methodName}`;
428
+ }
429
+ extractAssemblyName(test) {
430
+ // Extract assembly name from file path or use default
431
+ if (test.file) {
432
+ const parts = test.file.split(/[/\\]/);
433
+ return parts[0] || 'DefaultAssembly';
434
+ }
435
+ return 'DefaultAssembly';
436
+ }
437
+ extractNamespace(test) {
438
+ // Extract namespace from suite_title or classname
439
+ if (test.suite_title && test.suite_title.includes('.')) {
440
+ const parts = test.suite_title.split('.');
441
+ return parts.slice(0, -1).join('.');
442
+ }
443
+ return test.suite_title || 'DefaultNamespace';
444
+ }
445
+ extractClassName(test) {
446
+ // Extract class name from suite_title
447
+ if (test.suite_title && test.suite_title.includes('.')) {
448
+ const parts = test.suite_title.split('.');
449
+ return parts[parts.length - 1];
450
+ }
451
+ return test.suite_title || 'DefaultClass';
452
+ }
453
+ extractCsFileFromPath(test) {
454
+ // Extract .cs file name from source file path, not namespace
455
+ if (test.file) {
456
+ // Look for actual .cs file path patterns
457
+ const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
458
+ if (csFileMatch) {
459
+ return test.file;
460
+ }
461
+ // If no .cs extension, assume it's a namespace path and convert to likely file name
462
+ const className = this.extractClassName(test);
463
+ const pathParts = test.file.split(/[/\\]/);
464
+ pathParts[pathParts.length - 1] = `${className}.cs`;
465
+ return pathParts.join('/');
466
+ }
467
+ // Fallback to class name
468
+ const className = this.extractClassName(test);
469
+ return `${className}.cs`;
470
+ }
278
471
  calculateStats() {
279
472
  this.stats = {
280
473
  ...this.stats,
@@ -298,6 +491,11 @@ class XmlReader {
298
491
  return this.stats;
299
492
  }
300
493
  fetchSourceCode() {
494
+ // Skip source code fetching if disabled
495
+ if (this.disableSourceCodeFetching) {
496
+ debug('Source code fetching is disabled');
497
+ return;
498
+ }
301
499
  this.tests.forEach(t => {
302
500
  try {
303
501
  const file = this.adapter.getFilePath(t);
@@ -324,7 +522,9 @@ class XmlReader {
324
522
  return;
325
523
  }
326
524
  const contents = fs_1.default.readFileSync(file).toString();
327
- t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
525
+ // Try original test name first (for parameterized tests), fallback to regular title
526
+ const titleForLookup = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
527
+ t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, title: titleForLookup, lang: this.stats.language });
328
528
  if (t.code)
329
529
  debug('Fetched code for test %s', t.title);
330
530
  t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
@@ -430,7 +630,12 @@ function reduceTestCases(prev, item) {
430
630
  testCases
431
631
  .filter(t => !!t)
432
632
  .forEach(testCaseItem => {
433
- const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
633
+ // Simple file extraction (version 2.1.1 approach) with fallback to enhanced extraction
634
+ let file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
635
+ // If no file found with simple approach and we have enhanced extraction enabled, use it
636
+ if (!file && item.filepath) {
637
+ file = extractSourceFilePath(testCaseItem, item);
638
+ }
434
639
  let stack = '';
435
640
  let message = '';
436
641
  if (testCaseItem.error)
@@ -454,13 +659,28 @@ function reduceTestCases(prev, item) {
454
659
  // SpecFlow config
455
660
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
456
661
  let example = null;
457
- const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
662
+ // Smart suite title extraction to avoid duplicates
663
+ const suiteTitle = getSuiteTitle(testCaseItem, item, isParametrized, reduceOptions.suiteOrganization);
458
664
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
459
665
  tags ||= [];
460
- const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
461
- if (exampleMatches) {
462
- example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
463
- title = title.replace(/\(.*?\)/, '').trim();
666
+ // Store original test name for enhanced parameter extraction
667
+ const originalTestName = testCaseItem.name || testCaseItem.methodname;
668
+ // Enhanced NUnit-style arguments from <arguments> element
669
+ if (testCaseItem.arguments && testCaseItem.arguments.arg) {
670
+ const args = Array.isArray(testCaseItem.arguments.arg)
671
+ ? testCaseItem.arguments.arg
672
+ : [testCaseItem.arguments.arg];
673
+ example = args; // Store as array instead of object
674
+ // Remove parameters from title for NUnit tests
675
+ title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
676
+ }
677
+ else {
678
+ // Simple parameter extraction (version 2.1.1 approach)
679
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
680
+ if (exampleMatches) {
681
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
682
+ title = title.replace(/\(.*?\)/, '').trim();
683
+ }
464
684
  }
465
685
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
466
686
  if (!testId)
@@ -508,6 +728,7 @@ function reduceTestCases(prev, item) {
508
728
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
509
729
  status,
510
730
  title,
731
+ originalTestName, // Store original name for enhanced features
511
732
  root_suite_id: TESTOMATIO_SUITE,
512
733
  suite_title: suiteTitle,
513
734
  files,
@@ -516,6 +737,102 @@ function reduceTestCases(prev, item) {
516
737
  });
517
738
  return prev;
518
739
  }
740
+ function extractSourceFilePath(testCaseItem, item) {
741
+ // Priority order for file path extraction to match Test Explorer structure:
742
+ // 1. filepath attribute (direct .cs file path from NUnit)
743
+ // 2. fullname (contains full project path)
744
+ // 3. file attribute from test case
745
+ // 4. package (fallback)
746
+ // NUnit provides filepath attribute with actual .cs file path - use this first
747
+ if (item.filepath) {
748
+ // Clean up Windows/Unix path separators and ensure proper format
749
+ let filePath = item.filepath.replace(/\\/g, '/');
750
+ // Make relative to current working directory if absolute
751
+ if (path_1.default.isAbsolute(item.filepath)) {
752
+ const cwd = process.cwd().replace(/\\/g, '/');
753
+ if (filePath.startsWith(cwd)) {
754
+ filePath = path_1.default.relative(cwd, item.filepath).replace(/\\/g, '/');
755
+ }
756
+ else {
757
+ // Try to extract relative path from common patterns
758
+ const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
759
+ for (const pattern of commonPatterns) {
760
+ const index = filePath.lastIndexOf(pattern);
761
+ if (index !== -1) {
762
+ filePath = filePath.substring(index + 1);
763
+ break;
764
+ }
765
+ }
766
+ }
767
+ }
768
+ return filePath;
769
+ }
770
+ if (testCaseItem.file) {
771
+ let filePath = testCaseItem.file.replace(/\\/g, '/');
772
+ // Make relative to current working directory if absolute
773
+ if (path_1.default.isAbsolute(testCaseItem.file)) {
774
+ const cwd = process.cwd().replace(/\\/g, '/');
775
+ if (filePath.startsWith(cwd)) {
776
+ filePath = path_1.default.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
777
+ }
778
+ else {
779
+ // Try to extract relative path from common patterns
780
+ const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
781
+ for (const pattern of commonPatterns) {
782
+ const index = filePath.lastIndexOf(pattern);
783
+ if (index !== -1) {
784
+ filePath = filePath.substring(index + 1);
785
+ break;
786
+ }
787
+ }
788
+ }
789
+ }
790
+ return filePath;
791
+ }
792
+ if (item.fullname) {
793
+ // Extract actual file path from fullname if it contains path separators
794
+ const fullnameParts = item.fullname.split('.');
795
+ if (fullnameParts.length > 2) {
796
+ // For ParameterizedMethod, get the class name (not method name)
797
+ // Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
798
+ let namespaceParts, className;
799
+ if (item.type === 'ParameterizedMethod') {
800
+ // For parameterized methods, the last part is the method name, second-to-last is class
801
+ namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
802
+ className = fullnameParts[fullnameParts.length - 2]; // Get class name
803
+ }
804
+ else {
805
+ // For regular classes/fixtures
806
+ namespaceParts = fullnameParts.slice(1, -1); // Skip project name
807
+ className = fullnameParts[fullnameParts.length - 1];
808
+ }
809
+ return `${namespaceParts.join('/')}/${className}.cs`;
810
+ }
811
+ }
812
+ if (item.package)
813
+ return item.package.replace(/\\/g, '/');
814
+ // Fallback: construct from classname
815
+ if (testCaseItem.classname) {
816
+ const parts = testCaseItem.classname.split('.');
817
+ const className = parts[parts.length - 1];
818
+ const namespacePath = parts.slice(0, -1).join('/');
819
+ return `${namespacePath}/${className}.cs`;
820
+ }
821
+ return '';
822
+ }
823
+ function extractTestExplorerSuiteTitle(testCaseItem, item) {
824
+ // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
825
+ // Priority: fullname > classname > name
826
+ if (item.fullname) {
827
+ // Use fullname to maintain Test Explorer structure
828
+ return item.fullname;
829
+ }
830
+ if (testCaseItem.classname) {
831
+ return testCaseItem.classname;
832
+ }
833
+ // Fallback to item name but prefer classname structure
834
+ return item.name || testCaseItem.classname || 'UnknownClass';
835
+ }
519
836
  function processTestSuite(testsuite) {
520
837
  if (!testsuite)
521
838
  return [];
@@ -527,9 +844,38 @@ function processTestSuite(testsuite) {
527
844
  if (!Array.isArray(testsuite)) {
528
845
  suites = [testsuite];
529
846
  }
530
- const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
847
+ // Simple approach from version 2.1.1 with enhanced processing for complex scenarios
848
+ const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
531
849
  return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
532
850
  }
851
+ function getSuiteTitle(testCaseItem, item, isParametrized, suiteOrganization = 'classname') {
852
+ let suiteTitle;
853
+ if (suiteOrganization === 'fullpath') {
854
+ // Use full namespace path (old behavior that creates detailed structure)
855
+ if (item.fullname) {
856
+ return item.fullname;
857
+ }
858
+ suiteTitle = testCaseItem.classname || item.name;
859
+ }
860
+ else {
861
+ // Use classname approach (default - avoids duplicates)
862
+ if (isParametrized) {
863
+ // For parameterized tests, use the class name to group them
864
+ suiteTitle = item.name || testCaseItem.classname;
865
+ }
866
+ else {
867
+ // For regular tests, prefer classname over fullname to avoid long paths
868
+ suiteTitle = testCaseItem.classname || item.name;
869
+ }
870
+ // If still no suite title and we have fullname, extract just the class name
871
+ if (!suiteTitle && item.fullname) {
872
+ const fullnameParts = item.fullname.split('.');
873
+ suiteTitle = fullnameParts[fullnameParts.length - 1]; // Just the class name
874
+ }
875
+ }
876
+ // Fallback
877
+ return suiteTitle || 'UnknownClass';
878
+ }
533
879
  function fetchProperties(item) {
534
880
  const tags = [];
535
881
  let title = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.4",
3
+ "version": "2.3.5-beta-6-xml-import",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -3,18 +3,45 @@ 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
+ // Don't override example if it already exists from NUnit XML processing
7
+ // The xmlReader.js already extracts parameters correctly from <arguments>
8
+ if (!t.example) {
9
+ const title = t.title.replace(/\(.*?\)/, '').trim();
10
+ const exampleMatch = t.title.match(/\((.*?)\)/);
11
+ if (exampleMatch) {
12
+ // Keep as array for consistency with NUnit XML processing
13
+ t.example = exampleMatch[1].split(',').map(param => param.trim());
14
+ }
15
+ t.title = title.trim();
16
+ }
17
+
9
18
  const suite = t.suite_title.split('.');
10
19
  t.suite_title = suite.pop();
11
20
  t.file = namespaceToFileName(t.file);
12
- t.title = title.trim();
13
21
  return t;
14
22
  }
15
23
 
16
24
  getFilePath(t) {
17
- const fileName = namespaceToFileName(t.file);
25
+ if (!t.file) return null;
26
+
27
+ // Normalize path separators for cross-platform compatibility
28
+ let filePath = t.file.replace(/\\/g, '/');
29
+
30
+ // If file already has .cs extension, use it directly
31
+ if (filePath.endsWith('.cs')) {
32
+ // Make relative path if it's absolute
33
+ if (path.isAbsolute(filePath)) {
34
+ // Try to find project-relative path
35
+ const cwd = process.cwd().replace(/\\/g, '/');
36
+ if (filePath.startsWith(cwd)) {
37
+ filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
38
+ }
39
+ }
40
+ return filePath;
41
+ }
42
+
43
+ // Convert namespace path to file path
44
+ const fileName = namespaceToFileName(filePath);
18
45
  return fileName;
19
46
  }
20
47
  }
@@ -22,7 +49,14 @@ class CSharpAdapter extends Adapter {
22
49
  export default CSharpAdapter;
23
50
 
24
51
  function namespaceToFileName(fileName) {
52
+ if (!fileName) return '';
53
+
54
+ // If already a .cs file path, clean it up
55
+ if (fileName.endsWith('.cs')) {
56
+ return fileName.replace(/\\/g, '/');
57
+ }
58
+
25
59
  const fileParts = fileName.split('.');
26
60
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
27
- return `${fileParts.join(path.sep)}.cs`;
61
+ return `${fileParts.join('/')}.cs`;
28
62
  }
package/src/pipe/debug.js CHANGED
@@ -15,7 +15,7 @@ export class DebugPipe {
15
15
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
16
  if (this.isEnabled) {
17
17
  this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
18
+ isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,8 +93,7 @@ export class DebugPipe {
93
93
  const logData = { action: 'addTest', testId: data };
94
94
  if (this.store.runId) logData.runId = this.store.runId;
95
95
  this.logToFile(logData);
96
- }
97
- else this.batch.tests.push(data);
96
+ } else this.batch.tests.push(data);
98
97
 
99
98
  if (!this.batch.intervalFunction) await this.batchUpload();
100
99
  }