@testomatio/reporter 2.3.8-rc.1 → 2.3.8

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/README.md CHANGED
@@ -129,6 +129,7 @@ Bring this reporter on CI and never lose test results again!
129
129
  - [CSV](./docs/pipes/csv.md)
130
130
  - [HTML report](./docs/pipes/html.md)
131
131
  - [Bitbucket](./docs/pipes/bitbucket.md)
132
+ - 🔗 [Linking Tests](./docs/linking-tests.md)
132
133
  - 📓 [JUnit](./docs/junit.md)
133
134
  - 🗄️ [Artifacts](./docs/artifacts.md)
134
135
  - 🔂 [Workflows](./docs/workflows.md)
package/lib/bin/cli.js CHANGED
@@ -155,7 +155,7 @@ program
155
155
  .option('--lang <lang>', 'Language used (python, ruby, java)')
156
156
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
157
157
  .action(async (pattern, opts) => {
158
- if (!pattern.endsWith('.xml')) {
158
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
159
159
  pattern += '.xml';
160
160
  }
161
161
  let { javaTests, lang } = opts;
@@ -25,7 +25,7 @@ program
25
25
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
26
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
27
27
  .action(async (pattern, opts) => {
28
- if (!pattern.endsWith('.xml')) {
28
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
29
29
  pattern += '.xml';
30
30
  }
31
31
  let { javaTests, lang } = opts;
@@ -37,7 +37,10 @@ program
37
37
  lang = lang?.toLowerCase();
38
38
  if (javaTests === true || (lang === 'java' && !javaTests))
39
39
  javaTests = 'src/test/java';
40
- const runReader = new xmlReader_js_1.default({ javaTests, lang });
40
+ const runReader = new xmlReader_js_1.default({
41
+ javaTests,
42
+ lang,
43
+ });
41
44
  const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
42
45
  if (!files.length) {
43
46
  console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found 😥`);
File without changes
File without changes
@@ -1,5 +1,4 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
- getFilePath(t: any): string;
4
3
  }
5
4
  import Adapter from './adapter.js';
@@ -7,24 +7,57 @@ const path_1 = __importDefault(require("path"));
7
7
  const adapter_js_1 = __importDefault(require("./adapter.js"));
8
8
  class CSharpAdapter extends adapter_js_1.default {
9
9
  formatTest(t) {
10
- const title = t.title.replace(/\(.*?\)/, '').trim();
11
- const example = t.title.match(/\((.*?)\)/);
12
- if (example)
13
- t.example = { ...example[1].split(',') };
10
+ // Extract example from title if not already present
11
+ if (!t.example) {
12
+ const exampleMatch = t.title.match(/\((.*?)\)/);
13
+ if (exampleMatch) {
14
+ // Extract parameters as object with numeric keys for API
15
+ const params = exampleMatch[1].split(',').map(param => param.trim()).filter(param => param !== '');
16
+ t.example = {};
17
+ params.forEach((param, index) => {
18
+ t.example[index] = param;
19
+ });
20
+ }
21
+ }
22
+ // Remove parameters from title to avoid duplicates in Test Suite
23
+ // The example field will be used for grouping on import
24
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
14
25
  const suite = t.suite_title.split('.');
15
26
  t.suite_title = suite.pop();
16
27
  t.file = namespaceToFileName(t.file);
17
- t.title = title.trim();
18
28
  return t;
19
29
  }
20
30
  getFilePath(t) {
21
- const fileName = namespaceToFileName(t.file);
31
+ if (!t.file)
32
+ return null;
33
+ // Normalize path separators for cross-platform compatibility
34
+ let filePath = t.file.replace(/\\/g, '/');
35
+ // If file already has .cs extension, use it directly
36
+ if (filePath.endsWith('.cs')) {
37
+ // Make relative path if it's absolute
38
+ if (path_1.default.isAbsolute(filePath)) {
39
+ // Try to find project-relative path
40
+ const cwd = process.cwd().replace(/\\/g, '/');
41
+ if (filePath.startsWith(cwd)) {
42
+ filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
43
+ }
44
+ }
45
+ return filePath;
46
+ }
47
+ // Convert namespace path to file path
48
+ const fileName = namespaceToFileName(filePath);
22
49
  return fileName;
23
50
  }
24
51
  }
25
52
  module.exports = CSharpAdapter;
26
53
  function namespaceToFileName(fileName) {
54
+ if (!fileName)
55
+ return '';
56
+ // If already a .cs file path, clean it up
57
+ if (fileName.endsWith('.cs')) {
58
+ return fileName.replace(/\\/g, '/');
59
+ }
27
60
  const fileParts = fileName.split('.');
28
61
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
29
- return `${fileParts.join(path_1.default.sep)}.cs`;
62
+ return `${fileParts.join('/')}.cs`;
30
63
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
3
+ * and parameterized tests
4
+ */
5
+ export class NUnitXmlParser {
6
+ constructor(options?: {});
7
+ options: {};
8
+ tests: any[];
9
+ stats: {
10
+ total: number;
11
+ passed: number;
12
+ failed: number;
13
+ skipped: number;
14
+ inconclusive: number;
15
+ };
16
+ /**
17
+ * Parse NUnit XML test-run structure
18
+ * @param {Object} testRun - Parsed XML test-run object
19
+ * @returns {Object} - Parsed test results
20
+ */
21
+ parseTestRun(testRun: any): any;
22
+ /**
23
+ * Recursively parse test-suite elements based on their type
24
+ * @param {Object|Array} testSuite - Test suite object or array
25
+ * @param {Array} parentPath - Current path in the hierarchy
26
+ */
27
+ parseTestSuite(testSuite: any | any[], parentPath?: any[]): void;
28
+ /**
29
+ * Process child elements of a test suite
30
+ * @param {Object} testSuite - Test suite object
31
+ * @param {Array} currentPath - Current path in hierarchy
32
+ */
33
+ processChildren(testSuite: any, currentPath: any[]): void;
34
+ /**
35
+ * Parse test-case elements (actual tests)
36
+ * @param {Object|Array} testCases - Test case object or array
37
+ * @param {Array} suitePath - Path to the test suite
38
+ * @param {Object} parentSuite - Parent test suite for context
39
+ */
40
+ parseTestCases(testCases: any | any[], suitePath: any[], parentSuite: any): void;
41
+ /**
42
+ * Parse individual test case
43
+ * @param {Object} testCase - Test case object
44
+ * @param {Array} suitePath - Path to the test suite
45
+ * @param {Object} parentSuite - Parent test suite for context
46
+ * @returns {Object|null} - Parsed test object
47
+ */
48
+ parseTestCase(testCase: any, suitePath: any[], parentSuite: any): any | null;
49
+ /**
50
+ * Extract method name and parameters from test name
51
+ * @param {string} testName - Full test name
52
+ * @returns {Object} - Extracted information
53
+ */
54
+ extractParameters(testName: string): any;
55
+ /**
56
+ * Parse parameter string into array of parameters
57
+ * @param {string} paramString - Parameter string
58
+ * @returns {Array} - Array of parameters
59
+ */
60
+ parseParameterString(paramString: string): any[];
61
+ /**
62
+ * Extract method name from test name (fallback)
63
+ * @param {string} testName - Test name
64
+ * @returns {string} - Method name
65
+ */
66
+ extractMethodName(testName: string): string;
67
+ /**
68
+ * Build file path from suite path and class name
69
+ * @param {Array} suitePath - Suite path array
70
+ * @param {string} className - Class name
71
+ * @param {Object} parentSuite - Parent suite for context
72
+ * @returns {string} - File path
73
+ */
74
+ buildFilePath(suitePath: any[], className: string, parentSuite: any): string;
75
+ /**
76
+ * Group parameterized tests by base method name
77
+ * @param {Array} tests - Array of parsed tests
78
+ * @returns {Object} - Grouped tests
79
+ */
80
+ groupParameterizedTests(tests: any[]): any;
81
+ }
82
+ export default NUnitXmlParser;
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NUnitXmlParser = void 0;
7
+ const debug_1 = __importDefault(require("debug"));
8
+ const constants_js_1 = require("../constants.js");
9
+ const utils_js_1 = require("../utils/utils.js");
10
+ const debug = (0, debug_1.default)('@testomatio/reporter:nunit-parser');
11
+ /**
12
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
13
+ * and parameterized tests
14
+ */
15
+ class NUnitXmlParser {
16
+ constructor(options = {}) {
17
+ this.options = options;
18
+ this.tests = [];
19
+ this.stats = {
20
+ total: 0,
21
+ passed: 0,
22
+ failed: 0,
23
+ skipped: 0,
24
+ inconclusive: 0,
25
+ };
26
+ }
27
+ /**
28
+ * Parse NUnit XML test-run structure
29
+ * @param {Object} testRun - Parsed XML test-run object
30
+ * @returns {Object} - Parsed test results
31
+ */
32
+ parseTestRun(testRun) {
33
+ debug('Parsing NUnit test-run');
34
+ // Extract run-level statistics
35
+ this.stats = {
36
+ total: parseInt(testRun.total || 0, 10),
37
+ passed: parseInt(testRun.passed || 0, 10),
38
+ failed: parseInt(testRun.failed || 0, 10),
39
+ skipped: parseInt(testRun.skipped || 0, 10),
40
+ inconclusive: parseInt(testRun.inconclusive || 0, 10),
41
+ };
42
+ // Process the root test-suite
43
+ if (testRun['test-suite']) {
44
+ this.parseTestSuite(testRun['test-suite'], []);
45
+ }
46
+ debug(`Parsed ${this.tests.length} tests from NUnit XML`);
47
+ return {
48
+ status: testRun.result?.toLowerCase() || 'unknown',
49
+ create_tests: true,
50
+ tests_count: this.tests.length,
51
+ passed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.PASSED).length,
52
+ failed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length,
53
+ skipped_count: this.tests.filter(t => t.status === constants_js_1.STATUS.SKIPPED).length,
54
+ tests: this.tests,
55
+ };
56
+ }
57
+ /**
58
+ * Recursively parse test-suite elements based on their type
59
+ * @param {Object|Array} testSuite - Test suite object or array
60
+ * @param {Array} parentPath - Current path in the hierarchy
61
+ */
62
+ parseTestSuite(testSuite, parentPath = []) {
63
+ if (!testSuite)
64
+ return;
65
+ // Handle arrays of test suites
66
+ if (Array.isArray(testSuite)) {
67
+ testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
68
+ return;
69
+ }
70
+ const suiteType = testSuite.type;
71
+ const suiteName = testSuite.name;
72
+ const fullName = testSuite.fullname;
73
+ debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
74
+ switch (suiteType) {
75
+ case 'Assembly':
76
+ // Assembly level - ignore the name, just process children
77
+ debug('Processing Assembly level - ignoring name, processing children');
78
+ this.processChildren(testSuite, parentPath);
79
+ break;
80
+ case 'TestSuite':
81
+ // Namespace/grouping level - add to path but don't create test
82
+ debug(`Processing TestSuite level - adding '${suiteName}' to path`);
83
+ // Avoid adding duplicate suite names to the path
84
+ const newPath = parentPath[parentPath.length - 1] === suiteName ? [...parentPath] : [...parentPath, suiteName];
85
+ this.processChildren(testSuite, newPath);
86
+ break;
87
+ case 'TestFixture':
88
+ // Test class level - add to path and process test cases
89
+ debug(`Processing TestFixture level - test class '${suiteName}'`);
90
+ const testFixturePath = [...parentPath, suiteName];
91
+ this.processChildren(testSuite, testFixturePath);
92
+ break;
93
+ case 'ParameterizedMethod':
94
+ // Parameterized method level - process test cases directly
95
+ debug(`Processing ParameterizedMethod level - method '${suiteName}'`);
96
+ // Don't add to path, just process children directly
97
+ this.processChildren(testSuite, parentPath);
98
+ break;
99
+ default:
100
+ debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
101
+ const unknownPath = [...parentPath, suiteName];
102
+ this.processChildren(testSuite, unknownPath);
103
+ break;
104
+ }
105
+ }
106
+ /**
107
+ * Process child elements of a test suite
108
+ * @param {Object} testSuite - Test suite object
109
+ * @param {Array} currentPath - Current path in hierarchy
110
+ */
111
+ processChildren(testSuite, currentPath) {
112
+ // Process test-cases first (to maintain order)
113
+ if (testSuite['test-case']) {
114
+ this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
115
+ }
116
+ // Process nested test-suites
117
+ if (testSuite['test-suite']) {
118
+ this.parseTestSuite(testSuite['test-suite'], currentPath);
119
+ }
120
+ }
121
+ /**
122
+ * Parse test-case elements (actual tests)
123
+ * @param {Object|Array} testCases - Test case object or array
124
+ * @param {Array} suitePath - Path to the test suite
125
+ * @param {Object} parentSuite - Parent test suite for context
126
+ */
127
+ parseTestCases(testCases, suitePath, parentSuite) {
128
+ if (!testCases)
129
+ return;
130
+ // Handle arrays of test cases
131
+ if (!Array.isArray(testCases)) {
132
+ testCases = [testCases];
133
+ }
134
+ testCases.forEach(testCase => {
135
+ const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
136
+ if (parsedTest) {
137
+ this.tests.push(parsedTest);
138
+ }
139
+ });
140
+ }
141
+ /**
142
+ * Parse individual test case
143
+ * @param {Object} testCase - Test case object
144
+ * @param {Array} suitePath - Path to the test suite
145
+ * @param {Object} parentSuite - Parent test suite for context
146
+ * @returns {Object|null} - Parsed test object
147
+ */
148
+ parseTestCase(testCase, suitePath, parentSuite) {
149
+ if (!testCase || !testCase.name) {
150
+ debug('Skipping test case without name');
151
+ return null;
152
+ }
153
+ // Use Description from properties if available (for SpecFlow tests), otherwise use name
154
+ let testName = testCase.name;
155
+ if (testCase.properties && testCase.properties.property) {
156
+ const properties = Array.isArray(testCase.properties.property)
157
+ ? testCase.properties.property
158
+ : [testCase.properties.property];
159
+ const descriptionProperty = properties.find(p => p.name === 'Description');
160
+ if (descriptionProperty && descriptionProperty.value) {
161
+ // Clean up SpecFlow description format: [C211256] Allow mobile print behavior -> Allow mobile print behavior
162
+ testName = descriptionProperty.value.replace(/^\[[^\]]+\]\s*/, '');
163
+ }
164
+ }
165
+ const fullName = testCase.fullname;
166
+ const methodName = testCase.methodname || this.extractMethodName(testName);
167
+ const className = testCase.classname || parentSuite?.name;
168
+ debug(`Parsing test case: ${testName}`);
169
+ debug(`Test case structure:`, JSON.stringify(testCase, null, 2));
170
+ // Extract parameters if this is a parameterized test
171
+ const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
172
+ // Determine test status
173
+ let status = constants_js_1.STATUS.PASSED;
174
+ if (testCase.result) {
175
+ switch (testCase.result.toLowerCase()) {
176
+ case 'passed':
177
+ status = constants_js_1.STATUS.PASSED;
178
+ break;
179
+ case 'failed':
180
+ status = constants_js_1.STATUS.FAILED;
181
+ break;
182
+ case 'skipped':
183
+ case 'ignored':
184
+ status = constants_js_1.STATUS.SKIPPED;
185
+ break;
186
+ case 'inconclusive':
187
+ status = constants_js_1.STATUS.SKIPPED; // Treat inconclusive as skipped
188
+ break;
189
+ default:
190
+ status = constants_js_1.STATUS.PASSED;
191
+ }
192
+ }
193
+ // Extract error information
194
+ let message = '';
195
+ let stack = '';
196
+ const files = [];
197
+ // Extract attachments (NUnit format)
198
+ if (testCase.attachments) {
199
+ const attachments = Array.isArray(testCase.attachments.attachment)
200
+ ? testCase.attachments.attachment
201
+ : [testCase.attachments.attachment];
202
+ const attachmentFiles = attachments.filter(a => a && a.filePath).map(a => a.filePath);
203
+ files.push(...attachmentFiles);
204
+ }
205
+ if (testCase.failure) {
206
+ message = testCase.failure.message || '';
207
+ stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
208
+ }
209
+ if (testCase.output) {
210
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
211
+ const stackFiles = (0, utils_js_1.fetchFilesFromStackTrace)(outputText);
212
+ files.push(...stackFiles);
213
+ if (outputText) {
214
+ debug(`Found output in test case: ${outputText.substring(0, 100)}...`);
215
+ stack = `${stack}\n\n${outputText}`.trim();
216
+ }
217
+ else {
218
+ debug('No output text found in test case');
219
+ }
220
+ }
221
+ else {
222
+ debug('No output found in test case');
223
+ }
224
+ // Extract test ID and tags from properties
225
+ let testId = null;
226
+ let tags = [];
227
+ if (testCase.properties && testCase.properties.property) {
228
+ const properties = Array.isArray(testCase.properties.property)
229
+ ? testCase.properties.property
230
+ : [testCase.properties.property];
231
+ const idProperty = properties.find(p => p.name === 'ID');
232
+ if (idProperty) {
233
+ testId = idProperty.value;
234
+ // Remove @ and T prefixes if present
235
+ if (testId.startsWith('@'))
236
+ testId = testId.slice(1);
237
+ if (testId.startsWith('T'))
238
+ testId = testId.slice(1);
239
+ }
240
+ // Extract Category properties as tags
241
+ const categoryProperties = properties.filter(p => p.name === 'Category');
242
+ tags = categoryProperties.map(p => p.value);
243
+ }
244
+ // If no test ID found in properties, try to extract from output
245
+ if (!testId && testCase.output) {
246
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
247
+ if (outputText) {
248
+ debug(`Looking for test ID in output: ${outputText.substring(0, 200)}...`);
249
+ const idMatch = outputText.match(/\[ID\]\s+tid:\/\/@T([a-f0-9]{8})/i);
250
+ if (idMatch) {
251
+ testId = idMatch[1];
252
+ debug(`Found test ID in output: ${testId}`);
253
+ }
254
+ else {
255
+ debug('No test ID found in output');
256
+ }
257
+ }
258
+ }
259
+ // Build file path from suite path and class name
260
+ const filePath = this.buildFilePath(suitePath, className, parentSuite);
261
+ // For parameterized tests, format example as expected by Testomatio API
262
+ // Convert array of parameters to object with numeric keys
263
+ let example = null;
264
+ if (isParameterized && parameters.length > 0) {
265
+ example = {};
266
+ parameters.forEach((param, index) => {
267
+ example[index] = param;
268
+ });
269
+ }
270
+ return {
271
+ // For runs: use full test name with parameters (TestBooleanValue(true))
272
+ // For import: API will group by base name using the example field
273
+ title: testName, // Full name with parameters for run display
274
+ methodName: baseMethodName || methodName || testName,
275
+ fullName: fullName,
276
+ suitePath: suitePath,
277
+ suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
278
+ file: filePath,
279
+ files: files, // Array of files that will be attached
280
+ status: status,
281
+ message: message,
282
+ stack: stack,
283
+ run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
284
+ test_id: testId,
285
+ tags: tags, // Array of category tags from properties
286
+ create: true,
287
+ retry: false,
288
+ // Parameterized test metadata
289
+ example: example, // Parameters as object for API grouping
290
+ isParameterized: isParameterized,
291
+ parameters: parameters, // Keep original array for reference
292
+ baseMethodName: baseMethodName,
293
+ };
294
+ }
295
+ /**
296
+ * Extract method name and parameters from test name
297
+ * @param {string} testName - Full test name
298
+ * @returns {Object} - Extracted information
299
+ */
300
+ extractParameters(testName) {
301
+ const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
302
+ if (paramMatch) {
303
+ const baseMethodName = paramMatch[1].trim();
304
+ const paramString = paramMatch[2];
305
+ // Parse parameters - handle quoted strings and nested structures
306
+ const parameters = this.parseParameterString(paramString);
307
+ return {
308
+ baseMethodName: baseMethodName,
309
+ parameters: parameters,
310
+ isParameterized: true,
311
+ };
312
+ }
313
+ return {
314
+ baseMethodName: testName,
315
+ parameters: [],
316
+ isParameterized: false,
317
+ };
318
+ }
319
+ /**
320
+ * Parse parameter string into array of parameters
321
+ * @param {string} paramString - Parameter string
322
+ * @returns {Array} - Array of parameters
323
+ */
324
+ parseParameterString(paramString) {
325
+ const parameters = [];
326
+ let current = '';
327
+ let inQuotes = false;
328
+ let quoteChar = null;
329
+ let depth = 0;
330
+ for (let i = 0; i < paramString.length; i++) {
331
+ const char = paramString[i];
332
+ if (!inQuotes && (char === '"' || char === "'")) {
333
+ inQuotes = true;
334
+ quoteChar = char;
335
+ current += char;
336
+ }
337
+ else if (inQuotes && char === quoteChar) {
338
+ inQuotes = false;
339
+ quoteChar = null;
340
+ current += char;
341
+ }
342
+ else if (!inQuotes && char === '(') {
343
+ depth++;
344
+ current += char;
345
+ }
346
+ else if (!inQuotes && char === ')') {
347
+ depth--;
348
+ current += char;
349
+ }
350
+ else if (!inQuotes && char === ',' && depth === 0) {
351
+ parameters.push(current.trim());
352
+ current = '';
353
+ }
354
+ else {
355
+ current += char;
356
+ }
357
+ }
358
+ if (current.trim()) {
359
+ parameters.push(current.trim());
360
+ }
361
+ // Clean up parameters - remove quotes if they wrap the entire parameter and filter empty ones
362
+ return parameters.map(param => {
363
+ param = param.trim();
364
+ if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
365
+ return param.slice(1, -1);
366
+ }
367
+ return param;
368
+ }).filter(p => !!p);
369
+ }
370
+ /**
371
+ * Extract method name from test name (fallback)
372
+ * @param {string} testName - Test name
373
+ * @returns {string} - Method name
374
+ */
375
+ extractMethodName(testName) {
376
+ // Remove parameters if present
377
+ const paramMatch = testName.match(/^(.+?)\(/);
378
+ return paramMatch ? paramMatch[1].trim() : testName;
379
+ }
380
+ /**
381
+ * Build file path from suite path and class name
382
+ * @param {Array} suitePath - Suite path array
383
+ * @param {string} className - Class name
384
+ * @param {Object} parentSuite - Parent suite for context
385
+ * @returns {string} - File path
386
+ */
387
+ buildFilePath(suitePath, className, parentSuite) {
388
+ // Try to get file path from parent suite
389
+ if (parentSuite && parentSuite.filepath) {
390
+ return parentSuite.filepath;
391
+ }
392
+ // Build path from suite hierarchy
393
+ const pathParts = [...suitePath];
394
+ if (className && !pathParts.includes(className)) {
395
+ pathParts.push(className);
396
+ }
397
+ // Convert to file path format
398
+ return pathParts.join('/') + '.cs'; // Assume C# for NUnit
399
+ }
400
+ /**
401
+ * Group parameterized tests by base method name
402
+ * @param {Array} tests - Array of parsed tests
403
+ * @returns {Object} - Grouped tests
404
+ */
405
+ groupParameterizedTests(tests) {
406
+ const grouped = {};
407
+ tests.forEach(test => {
408
+ const key = test.isParameterized
409
+ ? `${test.suitePath.join('.')}.${test.baseMethodName}`
410
+ : `${test.suitePath.join('.')}.${test.title}`;
411
+ if (!grouped[key]) {
412
+ grouped[key] = {
413
+ baseTest: {
414
+ name: test.baseMethodName || test.title,
415
+ suitePath: test.suitePath,
416
+ suite_title: test.suite_title,
417
+ file: test.file,
418
+ isParameterized: test.isParameterized,
419
+ },
420
+ variations: [],
421
+ };
422
+ }
423
+ grouped[key].variations.push(test);
424
+ });
425
+ return grouped;
426
+ }
427
+ }
428
+ exports.NUnitXmlParser = NUnitXmlParser;
429
+ module.exports = NUnitXmlParser;
430
+
431
+ module.exports.NUnitXmlParser = NUnitXmlParser;
package/lib/uploader.js CHANGED
@@ -170,6 +170,10 @@ class S3Uploader {
170
170
  if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
171
171
  filePath = path_1.default.join(process.cwd(), filePath);
172
172
  }
173
+ // Normalize path separators for cross-platform compatibility
174
+ if (typeof filePath === 'string') {
175
+ filePath = filePath.replace(/\\/g, '/');
176
+ }
173
177
  const data = { rid, file: filePath, uploaded };
174
178
  const jsonLine = `${JSON.stringify(data)}\n`;
175
179
  fs_1.default.appendFileSync(tempFilePath, jsonLine);