@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.
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);
@@ -2,7 +2,6 @@ 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;
6
5
  export function cleanLatestRunId(): any;
7
6
  export function isSameTest(test: any, t: any): boolean;
8
7
  export function fetchSourceCode(contents: any, opts?: {}): string;
@@ -38,7 +38,6 @@ 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;
42
41
  exports.cleanLatestRunId = cleanLatestRunId;
43
42
  exports.formatStep = formatStep;
44
43
  exports.readLatestRunId = readLatestRunId;
@@ -122,12 +121,8 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
122
121
  .map(f => f[1].trim())
123
122
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
124
123
  .map(f => {
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;
124
+ // Normalize path separators for cross-platform compatibility
125
+ return f.replace(/\\/g, '/');
131
126
  });
132
127
  debug('Found files in stack trace: ', files);
133
128
  return files.filter(f => {
@@ -174,6 +169,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
174
169
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
175
170
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
176
171
  const fetchIdFromCode = (code, opts = {}) => {
172
+ if (!code)
173
+ return null;
177
174
  const comments = code
178
175
  .split('\n')
179
176
  .map(l => l.trim())
@@ -216,10 +213,29 @@ const fetchSourceCode = (contents, opts = {}) => {
216
213
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
217
214
  }
218
215
  else if (opts.lang === 'csharp') {
219
- if (lineIndex === -1)
220
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
221
- if (lineIndex === -1)
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) {
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
+ }
223
239
  }
224
240
  else {
225
241
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -228,7 +244,7 @@ const fetchSourceCode = (contents, opts = {}) => {
228
244
  if (opts.prepend) {
229
245
  lineIndex -= opts.prepend;
230
246
  }
231
- if (lineIndex) {
247
+ if (lineIndex !== -1 && lineIndex !== undefined) {
232
248
  const result = [];
233
249
  for (let i = lineIndex; i < lineIndex + limit; i++) {
234
250
  if (lines[i] === undefined)
@@ -271,6 +287,14 @@ const fetchSourceCode = (contents, opts = {}) => {
271
287
  break;
272
288
  if (opts.lang === 'java' && lines[i].includes(' class '))
273
289
  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;
274
298
  }
275
299
  result.push(lines[i]);
276
300
  }
@@ -470,17 +494,9 @@ function transformEnvVarToBoolean(value) {
470
494
  // if not recognized, return truthy if any value is set
471
495
  return Boolean(value);
472
496
  }
473
- function truncate(s, size = 255) {
474
- if (s.toString().trim().length < size) {
475
- return s.toString();
476
- }
477
- return `${s.toString().substring(0, size)}...`;
478
- }
479
497
 
480
498
  module.exports.getPackageVersion = getPackageVersion;
481
499
 
482
- module.exports.truncate = truncate;
483
-
484
500
  module.exports.cleanLatestRunId = cleanLatestRunId;
485
501
 
486
502
  module.exports.formatStep = formatStep;
@@ -19,25 +19,11 @@ declare class XmlReader {
19
19
  tests: any[];
20
20
  stats: {};
21
21
  uploader: S3Uploader;
22
+ enhancedNunit: boolean;
23
+ groupParameterized: boolean;
22
24
  version: any;
23
25
  connectAdapter(): import("./junit-adapter/adapter.js").default;
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
- };
26
+ parse(fileName: any): any;
41
27
  processJUnit(jsonSuite: any): {
42
28
  create_tests: boolean;
43
29
  duration: number;
@@ -49,15 +35,14 @@ declare class XmlReader {
49
35
  tests: any[];
50
36
  tests_count: number;
51
37
  };
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
- };
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;
61
46
  processTRX(jsonSuite: any): {
62
47
  status: string;
63
48
  create_tests: boolean;
package/lib/xmlReader.js CHANGED
@@ -11,6 +11,7 @@ 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");
14
15
  const utils_js_1 = require("./utils/utils.js");
15
16
  const index_js_1 = require("./pipe/index.js");
16
17
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
@@ -54,6 +55,9 @@ class XmlReader {
54
55
  this.stats = {};
55
56
  this.stats.language = opts.lang?.toLowerCase();
56
57
  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
57
61
  // @ts-ignore
58
62
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
59
63
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
@@ -102,7 +106,8 @@ class XmlReader {
102
106
  return this.processJUnit(jsonSuite);
103
107
  }
104
108
  processJUnit(jsonSuite) {
105
- const { testsuite, name, tests, failures, errors } = jsonSuite;
109
+ const { testsuite, name, failures, errors } = jsonSuite;
110
+ const tests = testsuite?.tests || jsonSuite.tests;
106
111
  reduceOptions.preferClassname = this.stats.language === 'python';
107
112
  const resultTests = processTestSuite(testsuite);
108
113
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
@@ -128,6 +133,13 @@ class XmlReader {
128
133
  };
129
134
  }
130
135
  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');
131
143
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
144
  reduceOptions.preferClassname = this.stats.language === 'python';
133
145
  const resultTests = processTestSuite(jsonSuite['test-suite']);
@@ -142,6 +154,43 @@ class XmlReader {
142
154
  tests: resultTests,
143
155
  };
144
156
  }
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
+ }
145
194
  processTRX(jsonSuite) {
146
195
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
147
196
  if (!Array.isArray(defs))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.6-beta.1-truncate-steps",
3
+ "version": "2.3.7-beta.1-xml-import",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -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, truncate, fileSystem } from '../utils/utils.js';
5
+ import { getTestomatIdFromTestTitle, 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';
@@ -441,7 +441,7 @@ function formatCodeceptStep(step) {
441
441
  if (!step) return null;
442
442
 
443
443
  const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
444
- const title = truncate(step); // Use built-in toString
444
+ const title = step.toString(); // Use built-in toString
445
445
  const duration = step.duration || 0; // Use built-in duration
446
446
 
447
447
  const formattedStep = {
@@ -469,11 +469,10 @@ function formatHookStep(step) {
469
469
  if (step.actor && step.name) {
470
470
  title = `${step.actor} ${step.name}`;
471
471
  if (step.args && step.args.length > 0) {
472
- const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
472
+ const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
473
473
  title += ` ${argsStr}`;
474
474
  }
475
475
  }
476
- title = truncate(title);
477
476
 
478
477
  return {
479
478
  category: 'hook',
@@ -482,6 +481,5 @@ function formatHookStep(step) {
482
481
  };
483
482
  }
484
483
 
485
-
486
484
  export { CodeceptReporter };
487
485
  export default CodeceptReporter;
@@ -34,7 +34,10 @@ program
34
34
  }
35
35
  lang = lang?.toLowerCase();
36
36
  if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
37
- const runReader = new XmlReader({ javaTests, lang });
37
+ const runReader = new XmlReader({
38
+ javaTests,
39
+ lang,
40
+ });
38
41
  const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
39
42
  if (!files.length) {
40
43
  console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
package/src/client.js CHANGED
@@ -10,7 +10,7 @@ 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, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
13
+ import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
14
14
  import { filesize as prettyBytes } from 'filesize';
15
15
 
16
16
  const debug = createDebugMessages('@testomatio/reporter:client');
@@ -387,7 +387,7 @@ class Client {
387
387
  */
388
388
  formatLogs({ error, steps, logs }) {
389
389
  error = error?.trim();
390
- logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
390
+ logs = logs?.trim();
391
391
 
392
392
  if (Array.isArray(steps)) {
393
393
  steps = steps
@@ -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
  }