@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-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.
@@ -178,30 +178,63 @@ const fetchSourceCode = (contents, opts = {}) => {
178
178
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
179
179
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
180
180
  } else if (opts.lang === 'csharp') {
181
- // Enhanced C# method detection for NUnit tests
182
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
181
+ // Find the method declaration line
182
+ let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
183
183
 
184
- if (lineIndex === -1) {
185
- lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
184
+ if (methodLineIndex === -1) {
185
+ methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
186
186
  }
187
187
 
188
- if (lineIndex === -1) {
189
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
188
+ if (methodLineIndex === -1) {
189
+ methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
190
190
  }
191
191
 
192
- // Look for TestCase or Test attributes above the method
193
- if (lineIndex === -1) {
194
- const testAttributeIndex = lines.findIndex((l, index) => {
195
- if (l.includes('[TestCase') || l.includes('[Test')) {
196
- // Check next few lines for the method
197
- const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
198
- const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
199
- return hasMethod;
192
+ // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
193
+ if (methodLineIndex !== -1) {
194
+ lineIndex = methodLineIndex;
195
+
196
+ // Scan upwards to find the start of attributes and comments
197
+ for (let i = methodLineIndex - 1; i >= 0; i--) {
198
+ const trimmedLine = lines[i].trim();
199
+
200
+ // Include [TestCase], [Test], and other attributes
201
+ if (trimmedLine.startsWith('[')) {
202
+ lineIndex = i;
203
+ continue;
204
+ }
205
+
206
+ // Include XML documentation comments
207
+ if (trimmedLine.startsWith('///')) {
208
+ lineIndex = i;
209
+ continue;
210
+ }
211
+
212
+ // Stop at empty lines (with some tolerance)
213
+ if (trimmedLine === '') {
214
+ // Check if next non-empty line is an attribute or comment
215
+ let hasMoreAttributes = false;
216
+ for (let j = i - 1; j >= 0; j--) {
217
+ const nextTrimmed = lines[j].trim();
218
+ if (nextTrimmed === '') continue;
219
+ if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
220
+ hasMoreAttributes = true;
221
+ lineIndex = j;
222
+ }
223
+ break;
224
+ }
225
+ if (!hasMoreAttributes) break;
226
+ continue;
227
+ }
228
+
229
+ // Stop at other method declarations or class-level elements
230
+ if (
231
+ trimmedLine.includes('public ') ||
232
+ trimmedLine.includes('private ') ||
233
+ trimmedLine.includes('protected ') ||
234
+ trimmedLine.includes('internal ')
235
+ ) {
236
+ if (!trimmedLine.startsWith('[')) break;
200
237
  }
201
- return false;
202
- });
203
- if (testAttributeIndex !== -1) {
204
- lineIndex = testAttributeIndex;
205
238
  }
206
239
  }
207
240
  } else {
@@ -215,9 +248,29 @@ const fetchSourceCode = (contents, opts = {}) => {
215
248
 
216
249
  if (lineIndex !== -1 && lineIndex !== undefined) {
217
250
  const result = [];
251
+ let braceDepth = 0; // Track brace depth for C# methods
252
+ let methodStartFound = false; // Flag to indicate we've found the method opening brace
253
+
218
254
  for (let i = lineIndex; i < lineIndex + limit; i++) {
219
255
  if (lines[i] === undefined) continue;
220
256
 
257
+ // Track brace depth for C# to stop after method closes
258
+ if (opts.lang === 'csharp') {
259
+ const line = lines[i];
260
+ // Count opening and closing braces
261
+ const openBraces = (line.match(/\{/g) || []).length;
262
+ const closeBraces = (line.match(/\}/g) || []).length;
263
+
264
+ if (openBraces > 0) methodStartFound = true;
265
+ braceDepth += openBraces - closeBraces;
266
+
267
+ // If we've started the method and depth returns to 0, method is complete
268
+ if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
269
+ result.push(lines[i]);
270
+ break;
271
+ }
272
+ }
273
+
221
274
  if (i > lineIndex + 2 && !opts.prepend) {
222
275
  // annotation
223
276
  if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
@@ -238,10 +291,18 @@ const fetchSourceCode = (contents, opts = {}) => {
238
291
  if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
239
292
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
240
293
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
241
- if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/)) break;
242
- if (opts.lang === 'csharp' && lines[i].includes(' public void ')) break;
243
- if (opts.lang === 'csharp' && lines[i].includes(' public async Task ')) break;
244
- if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public')) break;
294
+ // For C#, additional checks if brace tracking didn't stop us
295
+ if (opts.lang === 'csharp') {
296
+ const trimmed = lines[i].trim();
297
+ // Stop at attribute that marks beginning of next test
298
+ if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/)) break;
299
+ // Stop at XML documentation comments that belong to next method
300
+ if (trimmed.startsWith('///')) break;
301
+ // Stop at another method declaration
302
+ if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/)) break;
303
+ // Stop at class declaration
304
+ if (trimmed.includes(' class ') && trimmed.includes('public')) break;
305
+ }
245
306
  }
246
307
  result.push(lines[i]);
247
308
  }
@@ -454,8 +515,16 @@ function transformEnvVarToBoolean(value) {
454
515
  return Boolean(value);
455
516
  }
456
517
 
518
+ function truncate(s, size = 255) {
519
+ if (s.toString().trim().length < size) {
520
+ return s.toString();
521
+ }
522
+ return `${s.toString().substring(0, size)}...`;
523
+ }
524
+
457
525
  export {
458
526
  ansiRegExp,
527
+ truncate,
459
528
  cleanLatestRunId,
460
529
  isSameTest,
461
530
  fetchSourceCode,
package/src/xmlReader.js CHANGED
@@ -240,59 +240,20 @@ class XmlReader {
240
240
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
241
241
  if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
242
242
 
243
- const tests =
244
- defs.map(td => {
245
- const title = td.name.replace(/\(.*?\)/, '').trim();
246
- let example = td.name.match(/\((.*?)\)/);
247
- if (example) example = { ...example[1].split(',') };
248
- const suite = td.TestMethod.className.split(', ')[0].split('.');
249
- const suite_title = suite.pop();
250
- return {
251
- title,
252
- example,
253
- file: suite.join('/'),
254
- description: td.Description,
255
- suite_title,
256
- id: td.Execution.id,
257
- };
258
- }) || [];
243
+ // Parse test definitions
244
+ const tests = defs.map(td => this._parseTRXTestDefinition(td));
259
245
 
246
+ // Parse test results
260
247
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
261
248
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
262
249
 
263
- const results = result.map(td => ({
264
- id: td.executionId,
265
- // seconds are used in junit reports, but ms are used by testomatio
266
- run_time: parseFloat(td.duration) * 1000,
267
- status: td.outcome,
268
- stack: td.Output.StdOut,
269
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
270
- }));
271
-
272
- results.forEach(r => {
273
- const test = tests.find(t => t.id === r.id) || {};
274
- r.suite_title = test.suite_title;
275
- r.title = test.title?.trim();
276
- if (test.code) r.code = test.code;
277
- if (test.description) r.description = test.description;
278
- if (test.example) r.example = test.example;
279
- if (test.file) r.file = test.file;
280
- r.create = true;
281
- r.overwrite = true;
282
- if (r.status === 'Passed') r.status = STATUS.PASSED;
283
- if (r.status === 'Failed') r.status = STATUS.FAILED;
284
- if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
285
- delete r.id;
286
- });
250
+ const results = result.map(td => this._parseTRXTestResult(td, tests));
287
251
 
288
252
  debug(results);
289
253
 
290
254
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
291
-
292
255
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
293
-
294
- let status = STATUS.PASSED.toString();
295
- if (failed_count > 0) status = STATUS.FAILED;
256
+ const status = failed_count > 0 ? STATUS.FAILED : STATUS.PASSED.toString();
296
257
 
297
258
  this.tests = results.filter(t => !!t.title);
298
259
 
@@ -307,6 +268,67 @@ class XmlReader {
307
268
  };
308
269
  }
309
270
 
271
+ _parseTRXTestDefinition(td) {
272
+ const title = td.name.replace(/\(.*?\)/, '').trim();
273
+ const exampleMatch = td.name.match(/\((.*?)\)/);
274
+ const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
275
+
276
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
277
+ const suite_title = suite.pop();
278
+
279
+ // Convert namespace to file path for C#
280
+ const file = `${suite.join('/')}.cs`;
281
+
282
+ return {
283
+ title, // Base name without parameters for test import
284
+ example, // Parameters object for parameterized tests
285
+ file, // File path with .cs extension
286
+ description: td.Description,
287
+ suite_title,
288
+ id: td.Execution.id,
289
+ };
290
+ }
291
+
292
+ _parseTRXTestResult(td, tests) {
293
+ const test = tests.find(t => t.id === td.executionId) || {};
294
+
295
+ const result = {
296
+ suite_title: test.suite_title,
297
+ title: test.title?.trim(),
298
+ file: test.file,
299
+ description: test.description,
300
+ code: test.code,
301
+ run_time: parseFloat(td.duration) * 1000,
302
+ stack: td.Output?.StdOut || '',
303
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
304
+ create: true,
305
+ overwrite: true,
306
+ };
307
+
308
+ // Add example for parameterized tests
309
+ if (test.example) {
310
+ result.example = test.example;
311
+ }
312
+
313
+ // Map TRX status to Testomat.io status
314
+ result.status = this._mapTRXStatus(td.outcome);
315
+
316
+ return result;
317
+ }
318
+
319
+ _mapTRXStatus(outcome) {
320
+ switch (outcome) {
321
+ case 'Passed':
322
+ return STATUS.PASSED;
323
+ case 'Failed':
324
+ return STATUS.FAILED;
325
+ case 'Skipped':
326
+ return STATUS.SKIPPED;
327
+ default:
328
+ return STATUS.PASSED;
329
+ }
330
+ }
331
+
310
332
  processXUnit(assemblies) {
311
333
  const tests = [];
312
334