@testomatio/reporter 2.3.1-beta.1-dependency → 2.3.2-beta.3-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
@@ -131,7 +131,18 @@ 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
- this.tests = this.tests.concat(resultTests);
134
+ debug('Raw tests extracted from NUnit XML:', resultTests.length);
135
+ debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
136
+ // Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
137
+ const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
138
+ debug('Tests after deduplication:', deduplicatedTests.length);
139
+ debug('Deduplicated tests:', deduplicatedTests.map(t => ({
140
+ title: t.title,
141
+ examples: t.examples,
142
+ example: t.example,
143
+ file: t.file,
144
+ })));
145
+ this.tests = this.tests.concat(deduplicatedTests);
135
146
  return {
136
147
  status: result?.toLowerCase(),
137
148
  create_tests: true,
@@ -139,7 +150,7 @@ class XmlReader {
139
150
  passed_count: parseInt(passed, 10),
140
151
  failed_count: parseInt(failed, 10),
141
152
  skipped_count: parseInt(inconclusive + skipped, 10),
142
- tests: resultTests,
153
+ tests: deduplicatedTests,
143
154
  };
144
155
  }
145
156
  processTRX(jsonSuite) {
@@ -275,6 +286,171 @@ class XmlReader {
275
286
  tests,
276
287
  };
277
288
  }
289
+ deduplicateTestsByFQN(tests) {
290
+ const fqnMap = new Map();
291
+ tests.forEach(test => {
292
+ const fqn = this.generateNormalizedFQN(test);
293
+ if (fqnMap.has(fqn)) {
294
+ const existingTest = fqnMap.get(fqn);
295
+ // For parameterized tests, merge as Examples
296
+ if (test.example && Array.isArray(test.example) && test.example.length > 0) {
297
+ // Initialize examples array if it doesn't exist
298
+ if (!existingTest.examples) {
299
+ existingTest.examples = [];
300
+ // Add the existing test's example as the first item if it has parameters
301
+ if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
302
+ existingTest.examples.push({
303
+ parameters: existingTest.example,
304
+ status: existingTest.status,
305
+ run_time: existingTest.run_time,
306
+ message: existingTest.message,
307
+ stack: existingTest.stack,
308
+ });
309
+ // Clear the main test's example since it's now in examples array
310
+ delete existingTest.example;
311
+ }
312
+ }
313
+ // Add this test's execution as an example
314
+ existingTest.examples.push({
315
+ parameters: test.example,
316
+ status: test.status,
317
+ run_time: test.run_time,
318
+ message: test.message,
319
+ stack: test.stack,
320
+ });
321
+ // Update the main test status to reflect the worst status
322
+ if (test.status === 'failed' || existingTest.status === 'failed') {
323
+ existingTest.status = 'failed';
324
+ }
325
+ else if (test.status === 'skipped' && existingTest.status !== 'failed') {
326
+ existingTest.status = 'skipped';
327
+ }
328
+ // Update total run time
329
+ existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
330
+ }
331
+ else {
332
+ // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
333
+ if (test.test_id && !existingTest.test_id) {
334
+ existingTest.test_id = test.test_id;
335
+ }
336
+ // Keep the most complete test data
337
+ if (test.stack && !existingTest.stack) {
338
+ existingTest.stack = test.stack;
339
+ }
340
+ if (test.message && !existingTest.message) {
341
+ existingTest.message = test.message;
342
+ }
343
+ }
344
+ // Prefer Test Explorer structure (longer, more complete suite_title)
345
+ if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
346
+ existingTest.suite_title = test.suite_title;
347
+ }
348
+ // Always use the source file path if available
349
+ if (test.file && test.file.endsWith('.cs')) {
350
+ existingTest.file = test.file;
351
+ }
352
+ else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
353
+ existingTest.file = this.extractCsFileFromPath(test);
354
+ }
355
+ }
356
+ else {
357
+ // Fix file path to use proper .cs file names from source paths
358
+ if (!test.file || !test.file.endsWith('.cs')) {
359
+ test.file = this.extractCsFileFromPath(test);
360
+ }
361
+ fqnMap.set(fqn, test);
362
+ }
363
+ });
364
+ return Array.from(fqnMap.values());
365
+ }
366
+ generateFQN(test) {
367
+ // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
368
+ // Don't include assembly as it can vary between different test structures
369
+ const namespace = this.extractNamespace(test);
370
+ const className = this.extractClassName(test);
371
+ const methodName = test.title;
372
+ // Use the most complete namespace.class structure available
373
+ if (test.suite_title && test.suite_title.includes('.')) {
374
+ return `${test.suite_title}.${methodName}`;
375
+ }
376
+ return `${namespace}.${className}.${methodName}`;
377
+ }
378
+ generateNormalizedFQN(test) {
379
+ // Generate normalized FQN for deduplication by extracting the core namespace.class.method
380
+ // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
381
+ const fullClassName = test.suite_title || '';
382
+ const methodName = test.title;
383
+ // Extract the most specific namespace.class pattern
384
+ if (fullClassName.includes('.')) {
385
+ const parts = fullClassName.split('.');
386
+ if (parts.length >= 2) {
387
+ const className = parts[parts.length - 1];
388
+ // Look for common .NET namespace patterns and normalize them:
389
+ // TestProject.Tests.MyClass -> Tests.MyClass
390
+ // Tests.MyClass -> Tests.MyClass
391
+ // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
392
+ let normalizedNamespace = '';
393
+ for (let i = parts.length - 2; i >= 0; i--) {
394
+ const part = parts[i];
395
+ // Build namespace from right to left, excluding project names
396
+ if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
397
+ // Found a test namespace, use it as the normalized namespace
398
+ normalizedNamespace = part;
399
+ break;
400
+ }
401
+ else if (i === parts.length - 2) {
402
+ // If no test namespace found, use the immediate parent as namespace
403
+ normalizedNamespace = part;
404
+ }
405
+ }
406
+ return `${normalizedNamespace}.${className}.${methodName}`;
407
+ }
408
+ }
409
+ // Fallback for simple class names
410
+ return `${fullClassName}.${methodName}`;
411
+ }
412
+ extractAssemblyName(test) {
413
+ // Extract assembly name from file path or use default
414
+ if (test.file) {
415
+ const parts = test.file.split(/[/\\]/);
416
+ return parts[0] || 'DefaultAssembly';
417
+ }
418
+ return 'DefaultAssembly';
419
+ }
420
+ extractNamespace(test) {
421
+ // Extract namespace from suite_title or classname
422
+ if (test.suite_title && test.suite_title.includes('.')) {
423
+ const parts = test.suite_title.split('.');
424
+ return parts.slice(0, -1).join('.');
425
+ }
426
+ return test.suite_title || 'DefaultNamespace';
427
+ }
428
+ extractClassName(test) {
429
+ // Extract class name from suite_title
430
+ if (test.suite_title && test.suite_title.includes('.')) {
431
+ const parts = test.suite_title.split('.');
432
+ return parts[parts.length - 1];
433
+ }
434
+ return test.suite_title || 'DefaultClass';
435
+ }
436
+ extractCsFileFromPath(test) {
437
+ // Extract .cs file name from source file path, not namespace
438
+ if (test.file) {
439
+ // Look for actual .cs file path patterns
440
+ const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
441
+ if (csFileMatch) {
442
+ return test.file;
443
+ }
444
+ // If no .cs extension, assume it's a namespace path and convert to likely file name
445
+ const className = this.extractClassName(test);
446
+ const pathParts = test.file.split(/[/\\]/);
447
+ pathParts[pathParts.length - 1] = `${className}.cs`;
448
+ return pathParts.join('/');
449
+ }
450
+ // Fallback to class name
451
+ const className = this.extractClassName(test);
452
+ return `${className}.cs`;
453
+ }
278
454
  calculateStats() {
279
455
  this.stats = {
280
456
  ...this.stats,
@@ -320,11 +496,13 @@ class XmlReader {
320
496
  this.stats.language = 'csharp';
321
497
  }
322
498
  if (!fs_1.default.existsSync(file)) {
323
- debug('Failed to open file with the source code', file);
499
+ debug('Failed to open file with the source code: %s', file);
324
500
  return;
325
501
  }
326
502
  const contents = fs_1.default.readFileSync(file).toString();
327
- t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
503
+ // Use original test name for source code lookup, not humanized title
504
+ const originalTitle = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
505
+ t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, title: originalTitle, lang: this.stats.language });
328
506
  if (t.code)
329
507
  debug('Fetched code for test %s', t.title);
330
508
  t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
@@ -430,7 +608,8 @@ function reduceTestCases(prev, item) {
430
608
  testCases
431
609
  .filter(t => !!t)
432
610
  .forEach(testCaseItem => {
433
- const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
611
+ // Use consistent Test Explorer structure: prioritize fullname for file path
612
+ const file = extractSourceFilePath(testCaseItem, item);
434
613
  let stack = '';
435
614
  let message = '';
436
615
  if (testCaseItem.error)
@@ -450,17 +629,33 @@ function reduceTestCases(prev, item) {
450
629
  if (!message)
451
630
  message = stack.trim().split('\n')[0];
452
631
  const isParametrized = item.type === 'ParameterizedMethod';
453
- const preferClassname = reduceOptions.preferClassname || isParametrized;
454
632
  // SpecFlow config
455
633
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
456
634
  let example = null;
457
- const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
635
+ // Use consistent Test Explorer structure for suite title
636
+ const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
458
637
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
459
638
  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();
639
+ // Store original test name for parameter extraction
640
+ const originalTestName = testCaseItem.name || testCaseItem.methodname;
641
+ // Handle NUnit-style arguments from <arguments> element
642
+ if (testCaseItem.arguments && testCaseItem.arguments.arg) {
643
+ const args = Array.isArray(testCaseItem.arguments.arg)
644
+ ? testCaseItem.arguments.arg
645
+ : [testCaseItem.arguments.arg];
646
+ example = args; // Store as array instead of object
647
+ // Remove parameters from title for NUnit tests
648
+ title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
649
+ }
650
+ else {
651
+ // Fallback to parsing parameters from test name (SpecFlow, etc.)
652
+ const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
653
+ if (exampleMatches) {
654
+ // Extract and store parameters as Examples
655
+ const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
656
+ example = parameterValues;
657
+ title = title.replace(/\(.*?\)/, '').trim();
658
+ }
464
659
  }
465
660
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
466
661
  if (!testId)
@@ -508,6 +703,7 @@ function reduceTestCases(prev, item) {
508
703
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
509
704
  status,
510
705
  title,
706
+ originalTestName, // Store original name for parameter-aware FQN generation
511
707
  root_suite_id: TESTOMATIO_SUITE,
512
708
  suite_title: suiteTitle,
513
709
  files,
@@ -516,6 +712,102 @@ function reduceTestCases(prev, item) {
516
712
  });
517
713
  return prev;
518
714
  }
715
+ function extractSourceFilePath(testCaseItem, item) {
716
+ // Priority order for file path extraction to match Test Explorer structure:
717
+ // 1. filepath attribute (direct .cs file path from NUnit)
718
+ // 2. fullname (contains full project path)
719
+ // 3. file attribute from test case
720
+ // 4. package (fallback)
721
+ // NUnit provides filepath attribute with actual .cs file path - use this first
722
+ if (item.filepath) {
723
+ // Clean up Windows/Unix path separators and ensure proper format
724
+ let filePath = item.filepath.replace(/\\/g, '/');
725
+ // Make relative to current working directory if absolute
726
+ if (path_1.default.isAbsolute(item.filepath)) {
727
+ const cwd = process.cwd().replace(/\\/g, '/');
728
+ if (filePath.startsWith(cwd)) {
729
+ filePath = path_1.default.relative(cwd, item.filepath).replace(/\\/g, '/');
730
+ }
731
+ else {
732
+ // Try to extract relative path from common patterns
733
+ const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
734
+ for (const pattern of commonPatterns) {
735
+ const index = filePath.lastIndexOf(pattern);
736
+ if (index !== -1) {
737
+ filePath = filePath.substring(index + 1);
738
+ break;
739
+ }
740
+ }
741
+ }
742
+ }
743
+ return filePath;
744
+ }
745
+ if (testCaseItem.file) {
746
+ let filePath = testCaseItem.file.replace(/\\/g, '/');
747
+ // Make relative to current working directory if absolute
748
+ if (path_1.default.isAbsolute(testCaseItem.file)) {
749
+ const cwd = process.cwd().replace(/\\/g, '/');
750
+ if (filePath.startsWith(cwd)) {
751
+ filePath = path_1.default.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
752
+ }
753
+ else {
754
+ // Try to extract relative path from common patterns
755
+ const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
756
+ for (const pattern of commonPatterns) {
757
+ const index = filePath.lastIndexOf(pattern);
758
+ if (index !== -1) {
759
+ filePath = filePath.substring(index + 1);
760
+ break;
761
+ }
762
+ }
763
+ }
764
+ }
765
+ return filePath;
766
+ }
767
+ if (item.fullname) {
768
+ // Extract actual file path from fullname if it contains path separators
769
+ const fullnameParts = item.fullname.split('.');
770
+ if (fullnameParts.length > 2) {
771
+ // For ParameterizedMethod, get the class name (not method name)
772
+ // Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
773
+ let namespaceParts, className;
774
+ if (item.type === 'ParameterizedMethod') {
775
+ // For parameterized methods, the last part is the method name, second-to-last is class
776
+ namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
777
+ className = fullnameParts[fullnameParts.length - 2]; // Get class name
778
+ }
779
+ else {
780
+ // For regular classes/fixtures
781
+ namespaceParts = fullnameParts.slice(1, -1); // Skip project name
782
+ className = fullnameParts[fullnameParts.length - 1];
783
+ }
784
+ return `${namespaceParts.join('/')}/${className}.cs`;
785
+ }
786
+ }
787
+ if (item.package)
788
+ return item.package.replace(/\\/g, '/');
789
+ // Fallback: construct from classname
790
+ if (testCaseItem.classname) {
791
+ const parts = testCaseItem.classname.split('.');
792
+ const className = parts[parts.length - 1];
793
+ const namespacePath = parts.slice(0, -1).join('/');
794
+ return `${namespacePath}/${className}.cs`;
795
+ }
796
+ return '';
797
+ }
798
+ function extractTestExplorerSuiteTitle(testCaseItem, item) {
799
+ // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
800
+ // Priority: fullname > classname > name
801
+ if (item.fullname) {
802
+ // Use fullname to maintain Test Explorer structure
803
+ return item.fullname;
804
+ }
805
+ if (testCaseItem.classname) {
806
+ return testCaseItem.classname;
807
+ }
808
+ // Fallback to item name but prefer classname structure
809
+ return item.name || testCaseItem.classname || 'UnknownClass';
810
+ }
519
811
  function processTestSuite(testsuite) {
520
812
  if (!testsuite)
521
813
  return [];
@@ -527,8 +819,20 @@ function processTestSuite(testsuite) {
527
819
  if (!Array.isArray(testsuite)) {
528
820
  suites = [testsuite];
529
821
  }
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();
822
+ let allResults = [];
823
+ for (const suite of suites) {
824
+ // Process child test suites recursively (TestFixture, ParameterizedMethod, etc.)
825
+ if (suite['test-suite']) {
826
+ const childResults = processTestSuite(suite['test-suite']);
827
+ allResults = allResults.concat(childResults);
828
+ }
829
+ // Process direct test cases in this suite
830
+ if (suite['test-case'] || suite.testcase) {
831
+ const leafResults = reduceTestCases([], suite);
832
+ allResults = allResults.concat(leafResults);
833
+ }
834
+ }
835
+ return allResults;
532
836
  }
533
837
  function fetchProperties(item) {
534
838
  const tags = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.1-beta.1-dependency",
3
+ "version": "2.3.2-beta.3-xml-import",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -10,20 +10,13 @@
10
10
  "author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
11
11
  "license": "MIT",
12
12
  "type": "module",
13
- "overrides": {
14
- "tmp": "0.2.4",
15
- "external-editor": {
16
- "tmp": "0.2.4"
17
- },
18
- "follow-redirects": "1.15.6",
19
- "axios": "^1.7.0"
20
- },
21
13
  "dependencies": {
22
14
  "@aws-sdk/client-s3": "^3.279.0",
23
15
  "@aws-sdk/lib-storage": "^3.279.0",
24
- "@cucumber/cucumber": "^12.2.0",
16
+ "@cucumber/cucumber": "^10.9.0",
25
17
  "@octokit/rest": "^21.1.1",
26
18
  "aws-sdk": "^2.1072.0",
19
+ "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
27
20
  "callsite-record": "^4.1.4",
28
21
  "commander": "^12",
29
22
  "cross-spawn": "^7.0.3",
@@ -33,7 +26,6 @@
33
26
  "fast-xml-parser": "^4.4.1",
34
27
  "file-url": "3.0.0",
35
28
  "filesize": "^10.1.6",
36
- "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
37
29
  "glob": "^10.3",
38
30
  "handlebars": "^4.7.8",
39
31
  "has-flag": "^5.0.1",
@@ -93,7 +85,7 @@
93
85
  "@types/mocha": "^10.0.7",
94
86
  "@wdio/reporter": "^7.16.13",
95
87
  "chai": "^4.3.6",
96
- "codeceptjs": "^3.7.4",
88
+ "codeceptjs": "^3.6.5",
97
89
  "cucumber": "^6.0.7",
98
90
  "eslint": "^9.24.0",
99
91
  "eslint-config-prettier": "^8.3.0",
@@ -101,13 +93,13 @@
101
93
  "jasmine": "^5.2.0",
102
94
  "jest": "^27.4.7",
103
95
  "jsdom": "^22.1.0",
104
- "mocha": "^11.7.2",
96
+ "mocha": "^9.2.0",
105
97
  "mock-http-server": "^1.4.5",
106
98
  "pino": "^8.15.0",
107
99
  "prettier": "^3.2.5",
108
100
  "puppeteer": "^22.15.0",
109
101
  "typescript": "^5.5.4",
110
- "vitest": "^3.2.4"
102
+ "vitest": "^1.6.0"
111
103
  },
112
104
  "bin": {
113
105
  "@testomatio/reporter": "./lib/bin/cli.js",
@@ -121,28 +113,27 @@
121
113
  "require": "./lib/reporter.js",
122
114
  "types": "./types/types.d.ts"
123
115
  },
124
- "exports": {
125
- ".": {
126
- "import": "./src/reporter.js",
127
- "require": "./lib/reporter.js",
128
- "types": "./types/types.d.ts"
129
- },
130
- "./cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
131
- "./cypress": "./lib/adapter/cypress-plugin/index.js",
132
- "./lib/adapter/jasmine.js": "./lib/adapter/jasmine.js",
133
- "./jasmine": "./lib/adapter/jasmine.js",
134
- "./lib/adapter/jest.js": "./lib/adapter/jest.js",
135
- "./jest": "./lib/adapter/jest.js",
136
- "./lib/adapter/mocha/mocha.js": "./lib/adapter/mocha.js",
137
- "./mocha": "./lib/adapter/mocha.js",
138
- "./lib/adapter/playwright.js": "./lib/adapter/playwright.js",
139
- "./nightwatch": "./lib/adapter/nightwatch.js",
140
- "./playwright": "./lib/adapter/playwright.js",
141
- "./vitest": "./lib/adapter/vitest.js",
142
- "./lib/adapter/webdriver.js": "./lib/adapter/webdriver.js",
143
- "./lib/adapter/webdriver": "./lib/adapter/webdriver.js",
144
- "./webdriver": "./lib/adapter/webdriver.js",
145
- "./wdio": "./lib/adapter/webdriver.js"
146
- }
116
+ "./lib/adapter/codecept/codecept.js": "./lib/adapter/codecept.js",
117
+ "./lib/adapter/codecept": "./lib/adapter/codecept.js",
118
+ "./codecept": "./lib/adapter/codecept.js",
119
+ "./lib/adapter/cucumber/cucumber.js": "./lib/adapter/cucumber/current.js",
120
+ "./cucumber": "./lib/adapter/cucumber/current.js",
121
+ "./lib/adapter/cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
122
+ "./cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
123
+ "./cypress": "./lib/adapter/cypress-plugin/index.js",
124
+ "./lib/adapter/jasmine.js": "./lib/adapter/jasmine.js",
125
+ "./jasmine": "./lib/adapter/jasmine.js",
126
+ "./lib/adapter/jest.js": "./lib/adapter/jest.js",
127
+ "./jest": "./lib/adapter/jest.js",
128
+ "./lib/adapter/mocha/mocha.js": "./lib/adapter/mocha.js",
129
+ "./mocha": "./lib/adapter/mocha.js",
130
+ "./lib/adapter/playwright.js": "./lib/adapter/playwright.js",
131
+ "./nightwatch": "./lib/adapter/nightwatch.js",
132
+ "./playwright": "./lib/adapter/playwright.js",
133
+ "./vitest": "./lib/adapter/vitest.js",
134
+ "./lib/adapter/webdriver.js": "./lib/adapter/webdriver.js",
135
+ "./lib/adapter/webdriver": "./lib/adapter/webdriver.js",
136
+ "./webdriver": "./lib/adapter/webdriver.js",
137
+ "./wdio": "./lib/adapter/webdriver.js"
147
138
  }
148
139
  }
@@ -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,
18
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,7 +93,8 @@ 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
- } else this.batch.tests.push(data);
96
+ }
97
+ else this.batch.tests.push(data);
97
98
 
98
99
  if (!this.batch.intervalFunction) await this.batchUpload();
99
100
  }