@testomatio/reporter 2.3.6-beta.1-truncate-steps → 2.3.7-beta.1-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.
@@ -0,0 +1,391 @@
1
+ import createDebugMessages from 'debug';
2
+ import { STATUS } from '../constants.js';
3
+
4
+ const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
5
+
6
+ /**
7
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
8
+ * and parameterized tests
9
+ */
10
+ export class NUnitXmlParser {
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ this.tests = [];
14
+ this.stats = {
15
+ total: 0,
16
+ passed: 0,
17
+ failed: 0,
18
+ skipped: 0,
19
+ inconclusive: 0,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Parse NUnit XML test-run structure
25
+ * @param {Object} testRun - Parsed XML test-run object
26
+ * @returns {Object} - Parsed test results
27
+ */
28
+ parseTestRun(testRun) {
29
+ debug('Parsing NUnit test-run');
30
+
31
+ // Extract run-level statistics
32
+ this.stats = {
33
+ total: parseInt(testRun.total || 0, 10),
34
+ passed: parseInt(testRun.passed || 0, 10),
35
+ failed: parseInt(testRun.failed || 0, 10),
36
+ skipped: parseInt(testRun.skipped || 0, 10),
37
+ inconclusive: parseInt(testRun.inconclusive || 0, 10),
38
+ };
39
+
40
+ // Process the root test-suite
41
+ if (testRun['test-suite']) {
42
+ this.parseTestSuite(testRun['test-suite'], []);
43
+ }
44
+
45
+ debug(`Parsed ${this.tests.length} tests from NUnit XML`);
46
+
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 === STATUS.PASSED).length,
52
+ failed_count: this.tests.filter(t => t.status === STATUS.FAILED).length,
53
+ skipped_count: this.tests.filter(t => t.status === STATUS.SKIPPED).length,
54
+ tests: this.tests,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Recursively parse test-suite elements based on their type
60
+ * @param {Object|Array} testSuite - Test suite object or array
61
+ * @param {Array} parentPath - Current path in the hierarchy
62
+ */
63
+ parseTestSuite(testSuite, parentPath = []) {
64
+ if (!testSuite) return;
65
+
66
+ // Handle arrays of test suites
67
+ if (Array.isArray(testSuite)) {
68
+ testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
69
+ return;
70
+ }
71
+
72
+ const suiteType = testSuite.type;
73
+ const suiteName = testSuite.name;
74
+ const fullName = testSuite.fullname;
75
+
76
+ debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
77
+
78
+ switch (suiteType) {
79
+ case 'Assembly':
80
+ // Assembly level - ignore the name, just process children
81
+ debug('Processing Assembly level - ignoring name, processing children');
82
+ this.processChildren(testSuite, parentPath);
83
+ break;
84
+
85
+ case 'TestSuite':
86
+ // Namespace/grouping level - add to path but don't create test
87
+ debug(`Processing TestSuite level - adding '${suiteName}' to path`);
88
+ const newPath = [...parentPath, suiteName];
89
+ this.processChildren(testSuite, newPath);
90
+ break;
91
+
92
+ case 'TestFixture':
93
+ // Test class level - add to path and process test cases
94
+ debug(`Processing TestFixture level - test class '${suiteName}'`);
95
+ const testFixturePath = [...parentPath, suiteName];
96
+ this.processChildren(testSuite, testFixturePath);
97
+ break;
98
+
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
+ /**
108
+ * Process child elements of a test suite
109
+ * @param {Object} testSuite - Test suite object
110
+ * @param {Array} currentPath - Current path in hierarchy
111
+ */
112
+ processChildren(testSuite, currentPath) {
113
+ // Process nested test-suites
114
+ if (testSuite['test-suite']) {
115
+ this.parseTestSuite(testSuite['test-suite'], currentPath);
116
+ }
117
+
118
+ // Process test-cases
119
+ if (testSuite['test-case']) {
120
+ this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Parse test-case elements (actual tests)
126
+ * @param {Object|Array} testCases - Test case object or array
127
+ * @param {Array} suitePath - Path to the test suite
128
+ * @param {Object} parentSuite - Parent test suite for context
129
+ */
130
+ parseTestCases(testCases, suitePath, parentSuite) {
131
+ if (!testCases) return;
132
+
133
+ // Handle arrays of test cases
134
+ if (!Array.isArray(testCases)) {
135
+ testCases = [testCases];
136
+ }
137
+
138
+ testCases.forEach(testCase => {
139
+ const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
140
+ if (parsedTest) {
141
+ this.tests.push(parsedTest);
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Parse individual test case
148
+ * @param {Object} testCase - Test case object
149
+ * @param {Array} suitePath - Path to the test suite
150
+ * @param {Object} parentSuite - Parent test suite for context
151
+ * @returns {Object|null} - Parsed test object
152
+ */
153
+ parseTestCase(testCase, suitePath, parentSuite) {
154
+ if (!testCase || !testCase.name) {
155
+ debug('Skipping test case without name');
156
+ return null;
157
+ }
158
+
159
+ const testName = testCase.name;
160
+ const fullName = testCase.fullname;
161
+ const methodName = testCase.methodname || this.extractMethodName(testName);
162
+ const className = testCase.classname || parentSuite?.name;
163
+
164
+ debug(`Parsing test case: ${testName}`);
165
+
166
+ // Extract parameters if this is a parameterized test
167
+ const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
168
+
169
+ // Determine test status
170
+ let status = STATUS.PASSED;
171
+ if (testCase.result) {
172
+ switch (testCase.result.toLowerCase()) {
173
+ case 'passed':
174
+ status = STATUS.PASSED;
175
+ break;
176
+ case 'failed':
177
+ status = STATUS.FAILED;
178
+ break;
179
+ case 'skipped':
180
+ case 'ignored':
181
+ status = STATUS.SKIPPED;
182
+ break;
183
+ case 'inconclusive':
184
+ status = STATUS.SKIPPED; // Treat inconclusive as skipped
185
+ break;
186
+ default:
187
+ status = STATUS.PASSED;
188
+ }
189
+ }
190
+
191
+ // Extract error information
192
+ let message = '';
193
+ let stack = '';
194
+
195
+ if (testCase.failure) {
196
+ message = testCase.failure.message || '';
197
+ stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
198
+ }
199
+
200
+ if (testCase.output && testCase.output['#text']) {
201
+ stack = `${stack}\n\n${testCase.output['#text']}`.trim();
202
+ }
203
+
204
+ // Extract test ID from properties
205
+ let testId = null;
206
+ if (testCase.properties && testCase.properties.property) {
207
+ const properties = Array.isArray(testCase.properties.property)
208
+ ? testCase.properties.property
209
+ : [testCase.properties.property];
210
+
211
+ const idProperty = properties.find(p => p.name === 'ID');
212
+ if (idProperty) {
213
+ testId = idProperty.value;
214
+ // Remove @ and T prefixes if present
215
+ if (testId.startsWith('@')) testId = testId.slice(1);
216
+ if (testId.startsWith('T')) testId = testId.slice(1);
217
+ }
218
+ }
219
+
220
+ // Build file path from suite path and class name
221
+ const filePath = this.buildFilePath(suitePath, className, parentSuite);
222
+
223
+ return {
224
+ title: isParameterized ? testName : methodName || testName,
225
+ methodName: baseMethodName || methodName || testName,
226
+ fullName: fullName,
227
+ suitePath: suitePath,
228
+ suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
229
+ file: filePath,
230
+ status: status,
231
+ message: message,
232
+ stack: stack,
233
+ run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
234
+ test_id: testId,
235
+ create: true,
236
+ retry: false,
237
+ // Parameterized test metadata
238
+ isParameterized: isParameterized,
239
+ parameters: parameters,
240
+ baseMethodName: baseMethodName,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Extract method name and parameters from test name
246
+ * @param {string} testName - Full test name
247
+ * @returns {Object} - Extracted information
248
+ */
249
+ extractParameters(testName) {
250
+ const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
251
+
252
+ if (paramMatch) {
253
+ const baseMethodName = paramMatch[1].trim();
254
+ const paramString = paramMatch[2];
255
+
256
+ // Parse parameters - handle quoted strings and nested structures
257
+ const parameters = this.parseParameterString(paramString);
258
+
259
+ return {
260
+ baseMethodName: baseMethodName,
261
+ parameters: parameters,
262
+ isParameterized: true,
263
+ };
264
+ }
265
+
266
+ return {
267
+ baseMethodName: testName,
268
+ parameters: [],
269
+ isParameterized: false,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Parse parameter string into array of parameters
275
+ * @param {string} paramString - Parameter string
276
+ * @returns {Array} - Array of parameters
277
+ */
278
+ parseParameterString(paramString) {
279
+ const parameters = [];
280
+ let current = '';
281
+ let inQuotes = false;
282
+ let quoteChar = null;
283
+ let depth = 0;
284
+
285
+ for (let i = 0; i < paramString.length; i++) {
286
+ const char = paramString[i];
287
+
288
+ if (!inQuotes && (char === '"' || char === "'")) {
289
+ inQuotes = true;
290
+ quoteChar = char;
291
+ current += char;
292
+ } else if (inQuotes && char === quoteChar) {
293
+ inQuotes = false;
294
+ quoteChar = null;
295
+ current += char;
296
+ } else if (!inQuotes && char === '(') {
297
+ depth++;
298
+ current += char;
299
+ } else if (!inQuotes && char === ')') {
300
+ depth--;
301
+ current += char;
302
+ } else if (!inQuotes && char === ',' && depth === 0) {
303
+ parameters.push(current.trim());
304
+ current = '';
305
+ } else {
306
+ current += char;
307
+ }
308
+ }
309
+
310
+ if (current.trim()) {
311
+ parameters.push(current.trim());
312
+ }
313
+
314
+ // Clean up parameters - remove quotes if they wrap the entire parameter
315
+ return parameters.map(param => {
316
+ param = param.trim();
317
+ if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
318
+ return param.slice(1, -1);
319
+ }
320
+ return param;
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Extract method name from test name (fallback)
326
+ * @param {string} testName - Test name
327
+ * @returns {string} - Method name
328
+ */
329
+ extractMethodName(testName) {
330
+ // Remove parameters if present
331
+ const paramMatch = testName.match(/^(.+?)\(/);
332
+ return paramMatch ? paramMatch[1].trim() : testName;
333
+ }
334
+
335
+ /**
336
+ * Build file path from suite path and class name
337
+ * @param {Array} suitePath - Suite path array
338
+ * @param {string} className - Class name
339
+ * @param {Object} parentSuite - Parent suite for context
340
+ * @returns {string} - File path
341
+ */
342
+ buildFilePath(suitePath, className, parentSuite) {
343
+ // Try to get file path from parent suite
344
+ if (parentSuite && parentSuite.filepath) {
345
+ return parentSuite.filepath;
346
+ }
347
+
348
+ // Build path from suite hierarchy
349
+ const pathParts = [...suitePath];
350
+ if (className && !pathParts.includes(className)) {
351
+ pathParts.push(className);
352
+ }
353
+
354
+ // Convert to file path format
355
+ return pathParts.join('/') + '.cs'; // Assume C# for NUnit
356
+ }
357
+
358
+ /**
359
+ * Group parameterized tests by base method name
360
+ * @param {Array} tests - Array of parsed tests
361
+ * @returns {Object} - Grouped tests
362
+ */
363
+ groupParameterizedTests(tests) {
364
+ const grouped = {};
365
+
366
+ tests.forEach(test => {
367
+ const key = test.isParameterized
368
+ ? `${test.suitePath.join('.')}.${test.baseMethodName}`
369
+ : `${test.suitePath.join('.')}.${test.title}`;
370
+
371
+ if (!grouped[key]) {
372
+ grouped[key] = {
373
+ baseTest: {
374
+ name: test.baseMethodName || test.title,
375
+ suitePath: test.suitePath,
376
+ suite_title: test.suite_title,
377
+ file: test.file,
378
+ isParameterized: test.isParameterized,
379
+ },
380
+ variations: [],
381
+ };
382
+ }
383
+
384
+ grouped[key].variations.push(test);
385
+ });
386
+
387
+ return grouped;
388
+ }
389
+ }
390
+
391
+ export default NUnitXmlParser;
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
  }
@@ -20,7 +20,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
20
20
  class TestomatioPipe {
21
21
  constructor(params, store) {
22
22
  this.batch = {
23
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
23
+ isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
24
24
  intervalFunction: null, // will be created in createRun by setInterval function
25
25
  intervalTime: 5000, // how often tests are sent
26
26
  tests: [], // array of tests in batch
@@ -60,8 +60,8 @@ class TestomatioPipe {
60
60
  retryConfig: {
61
61
  retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
62
  retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
63
- httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
64
- shouldRetry: (error) => {
63
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
+ shouldRetry: error => {
65
65
  if (!error.response) return false;
66
66
  switch (error.response?.status) {
67
67
  case 400: // Bad request (probably wrong API key)
@@ -73,8 +73,8 @@ class TestomatioPipe {
73
73
  break;
74
74
  }
75
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- }
77
- }
76
+ },
77
+ },
78
78
  });
79
79
 
80
80
  this.isEnabled = true;
@@ -104,7 +104,6 @@ class TestomatioPipe {
104
104
  // add test ID + run ID
105
105
  if (data.rid) data.rid = `${this.runId}-${data.rid}`;
106
106
 
107
-
108
107
  if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
109
108
  data.stack = null;
110
109
  }
@@ -120,7 +119,6 @@ class TestomatioPipe {
120
119
  return data;
121
120
  }
122
121
 
123
-
124
122
  /**
125
123
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
126
124
  * @param {Object} opts - The options for preparing the test grepList.
@@ -215,7 +213,7 @@ class TestomatioPipe {
215
213
  method: 'PUT',
216
214
  url: `/api/reporter/${this.runId}`,
217
215
  data: runParams,
218
- responseType: 'json'
216
+ responseType: 'json',
219
217
  });
220
218
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
221
219
  return;
@@ -228,7 +226,7 @@ class TestomatioPipe {
228
226
  url: '/api/reporter',
229
227
  data: runParams,
230
228
  maxContentLength: Infinity,
231
- responseType: 'json'
229
+ responseType: 'json',
232
230
  });
233
231
 
234
232
  this.runId = resp.data.uid;
@@ -287,44 +285,44 @@ class TestomatioPipe {
287
285
 
288
286
  debug('Adding test', json);
289
287
 
290
- return this.client.request({
291
- method: 'POST',
292
- url: `/api/reporter/${this.runId}/testrun`,
293
- data: json,
294
- headers: {
295
- 'Content-Type': 'application/json',
296
- },
297
- maxContentLength: Infinity
298
- }).catch(err => {
299
- this.requestFailures++;
300
- this.notReportedTestsCount++;
301
- if (err.response) {
302
- if (err.response.status >= 400) {
303
- const responseData = err.response.data || { message: '' };
288
+ return this.client
289
+ .request({
290
+ method: 'POST',
291
+ url: `/api/reporter/${this.runId}/testrun`,
292
+ data: json,
293
+ headers: {
294
+ 'Content-Type': 'application/json',
295
+ },
296
+ maxContentLength: Infinity,
297
+ })
298
+ .catch(err => {
299
+ this.requestFailures++;
300
+ this.notReportedTestsCount++;
301
+ if (err.response) {
302
+ if (err.response.status >= 400) {
303
+ const responseData = err.response.data || { message: '' };
304
+ console.log(
305
+ APP_PREFIX,
306
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
+ pc.gray(data?.title || ''),
308
+ );
309
+ if (err.response?.data?.message?.includes('could not be matched')) {
310
+ this.hasUnmatchedTests = true;
311
+ }
312
+ return;
313
+ }
304
314
  console.log(
305
315
  APP_PREFIX,
306
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
- pc.gray(data?.title || ''),
316
+ pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
308
318
  );
309
- if (err.response?.data?.message?.includes('could not be matched')) {
310
- this.hasUnmatchedTests = true;
311
- }
312
- return;
319
+ printCreateIssue(err);
320
+ } else {
321
+ console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
313
322
  }
314
- console.log(
315
- APP_PREFIX,
316
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
- `Report couldn't be processed: ${err?.response?.data?.message}`,
318
- );
319
- printCreateIssue(err);
320
- } else {
321
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
322
- }
323
- });
323
+ });
324
324
  };
325
325
 
326
-
327
-
328
326
  /**
329
327
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
330
328
  */
@@ -349,43 +347,42 @@ class TestomatioPipe {
349
347
  const testsToSend = this.batch.tests.splice(0);
350
348
  debug('📨 Batch upload', testsToSend.length, 'tests');
351
349
 
352
- return this.client.request({
353
- method: 'POST',
354
- url: `/api/reporter/${this.runId}/testrun`,
355
- data: {
356
- api_key: this.apiKey,
357
- tests: testsToSend,
358
- batch_index: this.batch.batchIndex
359
- },
360
- headers: {
361
- 'Content-Type': 'application/json',
362
- },
363
- maxContentLength: Infinity
364
- }).catch(err => {
365
- this.requestFailures++;
366
- this.notReportedTestsCount += testsToSend.length;
367
- if (err.response) {
368
- if (err.response.status >= 400) {
369
- const responseData = err.response.data || { message: '' };
350
+ return this.client
351
+ .request({
352
+ method: 'POST',
353
+ url: `/api/reporter/${this.runId}/testrun`,
354
+ data: {
355
+ api_key: this.apiKey,
356
+ tests: testsToSend,
357
+ batch_index: this.batch.batchIndex,
358
+ },
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ },
362
+ maxContentLength: Infinity,
363
+ })
364
+ .catch(err => {
365
+ this.requestFailures++;
366
+ this.notReportedTestsCount += testsToSend.length;
367
+ if (err.response) {
368
+ if (err.response.status >= 400) {
369
+ const responseData = err.response.data || { message: '' };
370
+ console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
371
+ if (err.response?.data?.message?.includes('could not be matched')) {
372
+ this.hasUnmatchedTests = true;
373
+ }
374
+ return;
375
+ }
370
376
  console.log(
371
377
  APP_PREFIX,
372
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
378
+ pc.yellow(`Warning: (${err.response?.status})`),
379
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
373
380
  );
374
- if (err.response?.data?.message?.includes('could not be matched')) {
375
- this.hasUnmatchedTests = true;
376
- }
377
- return;
381
+ printCreateIssue(err);
382
+ } else {
383
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
378
384
  }
379
- console.log(
380
- APP_PREFIX,
381
- pc.yellow(`Warning: (${err.response?.status})`),
382
- `Report couldn't be processed: ${err?.response?.data?.message}`,
383
- );
384
- printCreateIssue(err);
385
- } else {
386
- console.log(APP_PREFIX, "Report couldn't be processed", err);
387
- }
388
- });
385
+ });
389
386
  };
390
387
 
391
388
  /**
@@ -408,9 +405,9 @@ class TestomatioPipe {
408
405
  else this.batch.tests.push(data);
409
406
 
410
407
  // if test is added after run which is already finished
411
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
408
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
412
409
 
413
- // return promise to be able to wait for it
410
+ // return promise to be able to wait for it
414
411
  return uploading;
415
412
  }
416
413
 
@@ -459,9 +456,11 @@ class TestomatioPipe {
459
456
  status_event,
460
457
  detach: params.detach,
461
458
  tests: params.tests,
462
- }
459
+ },
463
460
  });
464
461
 
462
+ debug(APP_PREFIX, '✅ Testrun finished');
463
+
465
464
  if (this.runUrl) {
466
465
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
467
466
  }
@@ -525,9 +524,6 @@ function printCreateIssue(err) {
525
524
  console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
526
525
  console.log('```');
527
526
  });
528
-
529
527
  }
530
528
 
531
-
532
-
533
529
  export default TestomatioPipe;
package/src/uploader.js CHANGED
@@ -194,6 +194,11 @@ export class S3Uploader {
194
194
  filePath = path.join(process.cwd(), filePath);
195
195
  }
196
196
 
197
+ // Normalize path separators for cross-platform compatibility
198
+ if (typeof filePath === 'string') {
199
+ filePath = filePath.replace(/\\/g, '/');
200
+ }
201
+
197
202
  const data = { rid, file: filePath, uploaded };
198
203
  const jsonLine = `${JSON.stringify(data)}\n`;
199
204
  fs.appendFileSync(tempFilePath, jsonLine);