@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-beta.100

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/uploader.js CHANGED
@@ -170,10 +170,6 @@ 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
- }
177
173
  const data = { rid, file: filePath, uploaded };
178
174
  const jsonLine = `${JSON.stringify(data)}\n`;
179
175
  fs_1.default.appendFileSync(tempFilePath, jsonLine);
@@ -2,6 +2,7 @@ export function getPackageVersion(): any;
2
2
  export const TEST_ID_REGEX: RegExp;
3
3
  export const SUITE_ID_REGEX: RegExp;
4
4
  export function ansiRegExp(): RegExp;
5
+ export function truncate(s: any, size?: number): any;
5
6
  export function cleanLatestRunId(): any;
6
7
  export function isSameTest(test: any, t: any): boolean;
7
8
  export function fetchSourceCode(contents: any, opts?: {}): string;
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
40
40
  exports.getPackageVersion = getPackageVersion;
41
+ exports.truncate = truncate;
41
42
  exports.cleanLatestRunId = cleanLatestRunId;
42
43
  exports.formatStep = formatStep;
43
44
  exports.readLatestRunId = readLatestRunId;
@@ -121,8 +122,12 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
121
122
  .map(f => f[1].trim())
122
123
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
123
124
  .map(f => {
124
- // Normalize path separators for cross-platform compatibility
125
- return f.replace(/\\/g, '/');
125
+ // Convert Windows paths to Linux paths for testing purposes
126
+ if (f.match(/^[A-Za-z]:[\\\/]/)) {
127
+ // Convert Windows path to Linux equivalent for test scenarios
128
+ return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
129
+ }
130
+ return f;
126
131
  });
127
132
  debug('Found files in stack trace: ', files);
128
133
  return files.filter(f => {
@@ -169,8 +174,6 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
169
174
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
170
175
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
171
176
  const fetchIdFromCode = (code, opts = {}) => {
172
- if (!code)
173
- return null;
174
177
  const comments = code
175
178
  .split('\n')
176
179
  .map(l => l.trim())
@@ -213,29 +216,10 @@ const fetchSourceCode = (contents, opts = {}) => {
213
216
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
214
217
  }
215
218
  else if (opts.lang === 'csharp') {
216
- // Enhanced C# method detection for NUnit tests
217
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
218
- if (lineIndex === -1) {
219
- lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
220
- }
221
- if (lineIndex === -1) {
219
+ if (lineIndex === -1)
220
+ lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
221
+ if (lineIndex === -1)
222
222
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
223
- }
224
- // Look for TestCase or Test attributes above the method
225
- if (lineIndex === -1) {
226
- const testAttributeIndex = lines.findIndex((l, index) => {
227
- if (l.includes('[TestCase') || l.includes('[Test')) {
228
- // Check next few lines for the method
229
- const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
230
- const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
231
- return hasMethod;
232
- }
233
- return false;
234
- });
235
- if (testAttributeIndex !== -1) {
236
- lineIndex = testAttributeIndex;
237
- }
238
- }
239
223
  }
240
224
  else {
241
225
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -244,7 +228,7 @@ const fetchSourceCode = (contents, opts = {}) => {
244
228
  if (opts.prepend) {
245
229
  lineIndex -= opts.prepend;
246
230
  }
247
- if (lineIndex !== -1 && lineIndex !== undefined) {
231
+ if (lineIndex) {
248
232
  const result = [];
249
233
  for (let i = lineIndex; i < lineIndex + limit; i++) {
250
234
  if (lines[i] === undefined)
@@ -287,14 +271,6 @@ const fetchSourceCode = (contents, opts = {}) => {
287
271
  break;
288
272
  if (opts.lang === 'java' && lines[i].includes(' class '))
289
273
  break;
290
- if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/))
291
- break;
292
- if (opts.lang === 'csharp' && lines[i].includes(' public void '))
293
- break;
294
- if (opts.lang === 'csharp' && lines[i].includes(' public async Task '))
295
- break;
296
- if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public'))
297
- break;
298
274
  }
299
275
  result.push(lines[i]);
300
276
  }
@@ -494,9 +470,21 @@ function transformEnvVarToBoolean(value) {
494
470
  // if not recognized, return truthy if any value is set
495
471
  return Boolean(value);
496
472
  }
473
+ function truncate(s, size = 255) {
474
+ if (s === undefined || s === null) {
475
+ return '';
476
+ }
477
+ const str = s.toString();
478
+ if (str.trim().length < size) {
479
+ return str;
480
+ }
481
+ return `${str.substring(0, size)}...`;
482
+ }
497
483
 
498
484
  module.exports.getPackageVersion = getPackageVersion;
499
485
 
486
+ module.exports.truncate = truncate;
487
+
500
488
  module.exports.cleanLatestRunId = cleanLatestRunId;
501
489
 
502
490
  module.exports.formatStep = formatStep;
@@ -19,11 +19,25 @@ declare class XmlReader {
19
19
  tests: any[];
20
20
  stats: {};
21
21
  uploader: S3Uploader;
22
- enhancedNunit: boolean;
23
- groupParameterized: boolean;
24
22
  version: any;
25
23
  connectAdapter(): import("./junit-adapter/adapter.js").default;
26
- parse(fileName: any): any;
24
+ parse(fileName: any): {
25
+ status: string;
26
+ create_tests: boolean;
27
+ tests_count: number;
28
+ passed_count: number;
29
+ skipped_count: number;
30
+ failed_count: number;
31
+ tests: any;
32
+ } | {
33
+ status: any;
34
+ create_tests: boolean;
35
+ tests_count: number;
36
+ passed_count: number;
37
+ failed_count: number;
38
+ skipped_count: number;
39
+ tests: any[];
40
+ };
27
41
  processJUnit(jsonSuite: any): {
28
42
  create_tests: boolean;
29
43
  duration: number;
@@ -35,14 +49,15 @@ declare class XmlReader {
35
49
  tests: any[];
36
50
  tests_count: number;
37
51
  };
38
- processNUnit(jsonSuite: any): any;
39
- /**
40
- * Check if the XML is actually NUnit format (has test-suite hierarchy)
41
- * @param {Object} jsonSuite - Parsed XML suite object
42
- * @returns {boolean} - True if this is NUnit XML format
43
- */
44
- isNUnitXml(jsonSuite: any): boolean;
45
- processNUnitEnhanced(jsonSuite: any): any;
52
+ processNUnit(jsonSuite: any): {
53
+ status: any;
54
+ create_tests: boolean;
55
+ tests_count: number;
56
+ passed_count: number;
57
+ failed_count: number;
58
+ skipped_count: number;
59
+ tests: any[];
60
+ };
46
61
  processTRX(jsonSuite: any): {
47
62
  status: string;
48
63
  create_tests: boolean;
package/lib/xmlReader.js CHANGED
@@ -11,7 +11,6 @@ const fast_xml_parser_1 = require("fast-xml-parser");
11
11
  const constants_js_1 = require("./constants.js");
12
12
  const crypto_1 = require("crypto");
13
13
  const url_1 = require("url");
14
- const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
15
14
  const utils_js_1 = require("./utils/utils.js");
16
15
  const index_js_1 = require("./pipe/index.js");
17
16
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
@@ -55,9 +54,6 @@ class XmlReader {
55
54
  this.stats = {};
56
55
  this.stats.language = opts.lang?.toLowerCase();
57
56
  this.uploader = new uploader_js_1.S3Uploader();
58
- // Enhanced NUnit parsing - enabled by default for NUnit XML
59
- this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
60
- this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
61
57
  // @ts-ignore
62
58
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
63
59
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
@@ -106,8 +102,7 @@ class XmlReader {
106
102
  return this.processJUnit(jsonSuite);
107
103
  }
108
104
  processJUnit(jsonSuite) {
109
- const { testsuite, name, failures, errors } = jsonSuite;
110
- const tests = testsuite?.tests || jsonSuite.tests;
105
+ const { testsuite, name, tests, failures, errors } = jsonSuite;
111
106
  reduceOptions.preferClassname = this.stats.language === 'python';
112
107
  const resultTests = processTestSuite(testsuite);
113
108
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
@@ -133,13 +128,6 @@ class XmlReader {
133
128
  };
134
129
  }
135
130
  processNUnit(jsonSuite) {
136
- // Use enhanced NUnit parser if enabled and this is actually NUnit XML
137
- if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
138
- debug('Using enhanced NUnit parser');
139
- return this.processNUnitEnhanced(jsonSuite);
140
- }
141
- // Fallback to legacy parser for backward compatibility
142
- debug('Using legacy NUnit parser');
143
131
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
144
132
  reduceOptions.preferClassname = this.stats.language === 'python';
145
133
  const resultTests = processTestSuite(jsonSuite['test-suite']);
@@ -154,43 +142,6 @@ class XmlReader {
154
142
  tests: resultTests,
155
143
  };
156
144
  }
157
- /**
158
- * Check if the XML is actually NUnit format (has test-suite hierarchy)
159
- * @param {Object} jsonSuite - Parsed XML suite object
160
- * @returns {boolean} - True if this is NUnit XML format
161
- */
162
- isNUnitXml(jsonSuite) {
163
- // NUnit XML has test-suite elements with type attributes
164
- if (jsonSuite['test-suite']) {
165
- const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
166
- // Check for NUnit-specific test-suite types
167
- return (testSuite &&
168
- testSuite.type &&
169
- ['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
170
- }
171
- return false;
172
- }
173
- processNUnitEnhanced(jsonSuite) {
174
- debug('Processing NUnit XML with enhanced parser');
175
- try {
176
- const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
177
- groupParameterized: this.groupParameterized,
178
- ...this.opts,
179
- });
180
- const result = nunitParser.parseTestRun(jsonSuite);
181
- // Add parsed tests to our collection
182
- this.tests = this.tests.concat(result.tests);
183
- debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
184
- return result;
185
- }
186
- catch (error) {
187
- debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
188
- console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
189
- // Fallback to legacy parser
190
- this.enhancedNunit = false;
191
- return this.processNUnit(jsonSuite);
192
- }
193
- }
194
145
  processTRX(jsonSuite) {
195
146
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
196
147
  if (!Array.isArray(defs))
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.7-beta.1-xml-import",
3
+ "version": "2.3.7-beta.100",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
7
7
  },
8
- "typings": "typings/index.d.ts",
8
+ "main": "lib/reporter.js",
9
+ "module": "src/reporter.js",
10
+ "types": "types/types.d.ts",
9
11
  "repository": "git@github.com:testomatio/reporter.git",
10
12
  "author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
11
13
  "license": "MIT",
@@ -45,7 +47,8 @@
45
47
  "bin",
46
48
  "lib",
47
49
  "src",
48
- "testcafe"
50
+ "testcafe",
51
+ "types"
49
52
  ],
50
53
  "scripts": {
51
54
  "clear-exportdir": "rm -rf export/",
@@ -57,7 +60,8 @@
57
60
  "test": "mocha 'tests/unit/**/*_test.js'",
58
61
  "test:playwright": "mocha tests/adapter/playwright.test.js",
59
62
  "test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
60
- "test:frameworks": "npm run test:playwright && npm run test:codecept",
63
+ "test:vitest": "mocha tests/adapter/vitest.test.js",
64
+ "test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest",
61
65
  "test:all": "npm run test && npm run test:frameworks",
62
66
  "test:adapters": "mocha tests/adapter/*.test.js",
63
67
  "test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
@@ -2,7 +2,7 @@ import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
3
  import TestomatClient from '../client.js';
4
4
  import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
5
- import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
5
+ import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
6
6
  import { services } from '../services/index.js';
7
7
  import { dataStorage } from '../data-storage.js';
8
8
  import codeceptjs from 'codeceptjs';
@@ -127,6 +127,28 @@ function CodeceptReporter(config) {
127
127
  });
128
128
 
129
129
 
130
+ // mark as failed all tests inside the failed hook
131
+ event.dispatcher.on(event.hook.failed, hook => {
132
+ if (hook.name !== 'BeforeSuiteHook') return;
133
+ const suite = hook.runnable.parent;
134
+
135
+ if (!suite) return;
136
+
137
+ const error = hook?.ctx?.currentTest?.err;
138
+
139
+ for (const test of suite.tests) {
140
+ client.addTestRun('failed', {
141
+ ...stripExampleFromTitle(test.title),
142
+ rid: test.uid,
143
+ test_id: getTestomatIdFromTestTitle(test.title),
144
+ suite_title: stripTagsFromTitle(suite.title),
145
+ error,
146
+ time: hook?.runnable?.duration,
147
+ });
148
+ }
149
+ });
150
+
151
+
130
152
  event.dispatcher.on(event.suite.before, suite => {
131
153
  dataStorage.setContext(suite.fullTitle());
132
154
  });
@@ -441,7 +463,7 @@ function formatCodeceptStep(step) {
441
463
  if (!step) return null;
442
464
 
443
465
  const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
444
- const title = step.toString(); // Use built-in toString
466
+ const title = truncate(step); // Use built-in toString
445
467
  const duration = step.duration || 0; // Use built-in duration
446
468
 
447
469
  const formattedStep = {
@@ -469,10 +491,11 @@ function formatHookStep(step) {
469
491
  if (step.actor && step.name) {
470
492
  title = `${step.actor} ${step.name}`;
471
493
  if (step.args && step.args.length > 0) {
472
- const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
494
+ const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
473
495
  title += ` ${argsStr}`;
474
496
  }
475
497
  }
498
+ title = truncate(title);
476
499
 
477
500
  return {
478
501
  category: 'hook',
@@ -481,5 +504,6 @@ function formatHookStep(step) {
481
504
  };
482
505
  }
483
506
 
507
+
484
508
  export { CodeceptReporter };
485
509
  export default CodeceptReporter;
package/src/bin/cli.js CHANGED
@@ -158,7 +158,7 @@ program
158
158
  .option('--lang <lang>', 'Language used (python, ruby, java)')
159
159
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
160
160
  .action(async (pattern, opts) => {
161
- if (!pattern.endsWith('.xml')) {
161
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
162
162
  pattern += '.xml';
163
163
  }
164
164
  let { javaTests, lang } = opts;
@@ -23,7 +23,7 @@ program
23
23
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
24
24
  .option('--env-file <envfile>', 'Load environment variables from env file')
25
25
  .action(async (pattern, opts) => {
26
- if (!pattern.endsWith('.xml')) {
26
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
27
27
  pattern += '.xml';
28
28
  }
29
29
  let { javaTests, lang } = opts;
@@ -18,7 +18,7 @@ const newArgs = ['run'];
18
18
  let i = 0;
19
19
  while (i < args.length) {
20
20
  const arg = args[i];
21
-
21
+
22
22
  if (arg === '-c' || arg === '--command') {
23
23
  // Map -c/--command to positional argument for run command
24
24
  i++;
@@ -33,7 +33,7 @@ while (i < args.length) {
33
33
  // Map --launch to start command
34
34
  newArgs[0] = 'start';
35
35
  } else if (arg === '--finish') {
36
- // Map --finish to finish command
36
+ // Map --finish to finish command
37
37
  newArgs[0] = 'finish';
38
38
  } else {
39
39
  // Pass through other arguments
@@ -45,9 +45,9 @@ while (i < args.length) {
45
45
  // Execute the main CLI with mapped arguments
46
46
 
47
47
  const child = spawn(process.execPath, [cliPath, ...newArgs], {
48
- stdio: 'inherit'
48
+ stdio: 'inherit',
49
49
  });
50
50
 
51
- child.on('exit', (code) => {
51
+ child.on('exit', code => {
52
52
  process.exit(code);
53
- });
53
+ });
package/src/client.js CHANGED
@@ -10,11 +10,21 @@ import { glob } from 'glob';
10
10
  import path, { sep } from 'path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { S3Uploader } from './uploader.js';
13
- import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
13
+ import {
14
+ formatStep,
15
+ truncate,
16
+ readLatestRunId,
17
+ storeRunId,
18
+ validateSuiteId,
19
+ transformEnvVarToBoolean
20
+ } from './utils/utils.js';
14
21
  import { filesize as prettyBytes } from 'filesize';
22
+ import { stripVTControlCharacters } from 'util';
15
23
 
16
24
  const debug = createDebugMessages('@testomatio/reporter:client');
17
25
 
26
+ const stripColors = stripVTControlCharacters || ((str) => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
27
+
18
28
  // removed __dirname usage, because:
19
29
  // 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
20
30
  // 2. got error "__dirname already defined" in compiles js code (cjs dir)
@@ -139,19 +149,6 @@ class Client {
139
149
  * @returns {Promise<PipeResult[]>}
140
150
  */
141
151
  async addTestRun(status, testData) {
142
- if (!this.pipes || !this.pipes.length)
143
- this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
144
-
145
- // all pipes disabled, skipping
146
- if (!this.pipes?.filter(p => p.isEnabled).length) return [];
147
-
148
- if (isTestShouldBeExculedFromReport(testData)) return [];
149
-
150
- if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
151
- debug('Skipping test from report', testData?.title);
152
- return []; // do not log skipped tests
153
- }
154
-
155
152
  if (!testData)
156
153
  testData = {
157
154
  title: 'Unknown test',
@@ -169,15 +166,23 @@ class Client {
169
166
  const {
170
167
  rid,
171
168
  error = null,
169
+ steps: originalSteps,
170
+ title,
171
+ suite_title,
172
+ } = testData;
173
+ let steps = originalSteps;
174
+
175
+ const uploadedFiles = [];
176
+ const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
177
+
178
+
179
+ const {
172
180
  time = 0,
173
181
  example = null,
174
182
  files = [],
175
183
  filesBuffers = [],
176
- steps,
177
184
  code = null,
178
- title,
179
185
  file,
180
- suite_title,
181
186
  suite_id,
182
187
  test_id,
183
188
  timestamp,
@@ -188,7 +193,6 @@ class Client {
188
193
  } = testData;
189
194
  let { message = '', meta = {} } = testData;
190
195
 
191
- // stringify meta values and limit keys and values length to 255
192
196
  meta = Object.entries(meta)
193
197
  .filter(([, value]) => value !== null && value !== undefined)
194
198
  .reduce((acc, [key, value]) => {
@@ -196,7 +200,6 @@ class Client {
196
200
  return acc;
197
201
  }, {});
198
202
 
199
- // Get links from storage using the test context
200
203
  const testContext = suite_title ? `${suite_title} ${title}` : title;
201
204
 
202
205
  let errorFormatted = '';
@@ -205,13 +208,38 @@ class Client {
205
208
  message = error?.message;
206
209
  }
207
210
 
208
- // Attach logs
209
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
211
+ let fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
210
212
 
211
- // add artifacts
212
- if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
213
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
214
+ uploadedFiles.push(
215
+ this.uploader.uploadFileAsBuffer(
216
+ Buffer.from(stripColors(fullLogs), 'utf8'),
217
+ [this.runId, rid, `logs_${+new Date}.log`]
218
+ )
219
+ );
220
+ fullLogs = '';
221
+ steps = null;
222
+ }
213
223
 
214
- const uploadedFiles = [];
224
+
225
+ if (!this.pipes || !this.pipes.length)
226
+ this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
227
+
228
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
229
+ if (uploadedFiles.length > 0) {
230
+ await Promise.all(uploadedFiles);
231
+ }
232
+ return [];
233
+ }
234
+
235
+ if (isTestShouldBeExculedFromReport(testData)) return [];
236
+
237
+ if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
238
+ debug('Skipping test from report', testData?.title);
239
+ return [];
240
+ }
241
+
242
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
215
243
 
216
244
  for (let f of files) {
217
245
  if (!f) continue; // f === null
@@ -308,7 +336,7 @@ class Client {
308
336
  const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
309
337
  relativePath: file.path.replace(process.cwd(), ''),
310
338
  link: file.link,
311
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
339
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
312
340
  }));
313
341
 
314
342
  uploadedArtifacts.forEach(upload => {
@@ -330,7 +358,7 @@ class Client {
330
358
  );
331
359
  const failedUploads = this.uploader.failedUploads.map(file => ({
332
360
  relativePath: file.path.replace(process.cwd(), ''),
333
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
361
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
334
362
  }));
335
363
 
336
364
  const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
@@ -387,7 +415,7 @@ class Client {
387
415
  */
388
416
  formatLogs({ error, steps, logs }) {
389
417
  error = error?.trim();
390
- logs = logs?.trim();
418
+ logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
391
419
 
392
420
  if (Array.isArray(steps)) {
393
421
  steps = steps
@@ -3,18 +3,23 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- // Don't override example if it already exists from NUnit XML processing
7
- // The xmlReader.js already extracts parameters correctly from <arguments>
6
+ // Extract example from title if not already present
8
7
  if (!t.example) {
9
- const title = t.title.replace(/\(.*?\)/, '').trim();
10
8
  const exampleMatch = t.title.match(/\((.*?)\)/);
11
9
  if (exampleMatch) {
12
- // Keep as array for consistency with NUnit XML processing
13
- t.example = exampleMatch[1].split(',').map(param => param.trim());
10
+ // Extract parameters as object with numeric keys for API
11
+ const params = exampleMatch[1].split(',').map(param => param.trim());
12
+ t.example = {};
13
+ params.forEach((param, index) => {
14
+ t.example[index] = param;
15
+ });
14
16
  }
15
- t.title = title.trim();
16
17
  }
17
18
 
19
+ // Remove parameters from title to avoid duplicates in Test Suite
20
+ // The example field will be used for grouping on import
21
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
22
+
18
23
  const suite = t.suite_title.split('.');
19
24
  t.suite_title = suite.pop();
20
25
  t.file = namespaceToFileName(t.file);