@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.
@@ -1,5 +1,6 @@
1
1
  import createDebugMessages from 'debug';
2
2
  import { STATUS } from '../constants.js';
3
+ import { fetchFilesFromStackTrace } from '../utils/utils.js';
3
4
 
4
5
  const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
5
6
 
@@ -85,7 +86,8 @@ export class NUnitXmlParser {
85
86
  case 'TestSuite':
86
87
  // Namespace/grouping level - add to path but don't create test
87
88
  debug(`Processing TestSuite level - adding '${suiteName}' to path`);
88
- const newPath = [...parentPath, suiteName];
89
+ // Avoid adding duplicate suite names to the path
90
+ const newPath = parentPath[parentPath.length - 1] === suiteName ? [...parentPath] : [...parentPath, suiteName];
89
91
  this.processChildren(testSuite, newPath);
90
92
  break;
91
93
 
@@ -96,6 +98,13 @@ export class NUnitXmlParser {
96
98
  this.processChildren(testSuite, testFixturePath);
97
99
  break;
98
100
 
101
+ case 'ParameterizedMethod':
102
+ // Parameterized method level - process test cases directly
103
+ debug(`Processing ParameterizedMethod level - method '${suiteName}'`);
104
+ // Don't add to path, just process children directly
105
+ this.processChildren(testSuite, parentPath);
106
+ break;
107
+
99
108
  default:
100
109
  debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
101
110
  const unknownPath = [...parentPath, suiteName];
@@ -110,15 +119,15 @@ export class NUnitXmlParser {
110
119
  * @param {Array} currentPath - Current path in hierarchy
111
120
  */
112
121
  processChildren(testSuite, currentPath) {
122
+ // Process test-cases first (to maintain order)
123
+ if (testSuite['test-case']) {
124
+ this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
125
+ }
126
+
113
127
  // Process nested test-suites
114
128
  if (testSuite['test-suite']) {
115
129
  this.parseTestSuite(testSuite['test-suite'], currentPath);
116
130
  }
117
-
118
- // Process test-cases
119
- if (testSuite['test-case']) {
120
- this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
121
- }
122
131
  }
123
132
 
124
133
  /**
@@ -156,12 +165,26 @@ export class NUnitXmlParser {
156
165
  return null;
157
166
  }
158
167
 
159
- const testName = testCase.name;
168
+ // Use Description from properties if available (for SpecFlow tests), otherwise use name
169
+ let testName = testCase.name;
170
+ if (testCase.properties && testCase.properties.property) {
171
+ const properties = Array.isArray(testCase.properties.property)
172
+ ? testCase.properties.property
173
+ : [testCase.properties.property];
174
+
175
+ const descriptionProperty = properties.find(p => p.name === 'Description');
176
+ if (descriptionProperty && descriptionProperty.value) {
177
+ // Clean up SpecFlow description format: [C211256] Allow mobile print behavior -> Allow mobile print behavior
178
+ testName = descriptionProperty.value.replace(/^\[[^\]]+\]\s*/, '');
179
+ }
180
+ }
181
+
160
182
  const fullName = testCase.fullname;
161
183
  const methodName = testCase.methodname || this.extractMethodName(testName);
162
184
  const className = testCase.classname || parentSuite?.name;
163
185
 
164
186
  debug(`Parsing test case: ${testName}`);
187
+ debug(`Test case structure:`, JSON.stringify(testCase, null, 2));
165
188
 
166
189
  // Extract parameters if this is a parameterized test
167
190
  const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
@@ -192,17 +215,31 @@ export class NUnitXmlParser {
192
215
  let message = '';
193
216
  let stack = '';
194
217
 
218
+ const files = [];
219
+
195
220
  if (testCase.failure) {
196
221
  message = testCase.failure.message || '';
197
222
  stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
198
223
  }
199
224
 
200
- if (testCase.output && testCase.output['#text']) {
201
- stack = `${stack}\n\n${testCase.output['#text']}`.trim();
225
+ if (testCase.output) {
226
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
227
+ const stackFiles = fetchFilesFromStackTrace(outputText);
228
+ files.push(...stackFiles);
229
+
230
+ if (outputText) {
231
+ debug(`Found output in test case: ${outputText.substring(0, 100)}...`);
232
+ stack = `${stack}\n\n${outputText}`.trim();
233
+ } else {
234
+ debug('No output text found in test case');
235
+ }
236
+ } else {
237
+ debug('No output found in test case');
202
238
  }
203
239
 
204
- // Extract test ID from properties
240
+ // Extract test ID and tags from properties
205
241
  let testId = null;
242
+ let tags = [];
206
243
  if (testCase.properties && testCase.properties.property) {
207
244
  const properties = Array.isArray(testCase.properties.property)
208
245
  ? testCase.properties.property
@@ -215,28 +252,62 @@ export class NUnitXmlParser {
215
252
  if (testId.startsWith('@')) testId = testId.slice(1);
216
253
  if (testId.startsWith('T')) testId = testId.slice(1);
217
254
  }
255
+
256
+ // Extract Category properties as tags
257
+ const categoryProperties = properties.filter(p => p.name === 'Category');
258
+ tags = categoryProperties.map(p => p.value);
259
+ }
260
+
261
+ // If no test ID found in properties, try to extract from output
262
+ if (!testId && testCase.output) {
263
+ const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
264
+ if (outputText) {
265
+ debug(`Looking for test ID in output: ${outputText.substring(0, 200)}...`);
266
+ const idMatch = outputText.match(/\[ID\]\s+tid:\/\/@T([a-f0-9]{8})/i);
267
+ if (idMatch) {
268
+ testId = idMatch[1];
269
+ debug(`Found test ID in output: ${testId}`);
270
+ } else {
271
+ debug('No test ID found in output');
272
+ }
273
+ }
218
274
  }
219
275
 
220
276
  // Build file path from suite path and class name
221
277
  const filePath = this.buildFilePath(suitePath, className, parentSuite);
222
278
 
279
+ // For parameterized tests, format example as expected by Testomatio API
280
+ // Convert array of parameters to object with numeric keys
281
+ let example = null;
282
+ if (isParameterized && parameters.length > 0) {
283
+ example = {};
284
+ parameters.forEach((param, index) => {
285
+ example[index] = param;
286
+ });
287
+ }
288
+
223
289
  return {
224
- title: isParameterized ? testName : methodName || testName,
290
+ // For runs: use full test name with parameters (TestBooleanValue(true))
291
+ // For import: API will group by base name using the example field
292
+ title: testName, // Full name with parameters for run display
225
293
  methodName: baseMethodName || methodName || testName,
226
294
  fullName: fullName,
227
295
  suitePath: suitePath,
228
296
  suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
229
297
  file: filePath,
298
+ files: files, // Array of files that will be attached
230
299
  status: status,
231
300
  message: message,
232
301
  stack: stack,
233
302
  run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
234
303
  test_id: testId,
304
+ tags: tags, // Array of category tags from properties
235
305
  create: true,
236
306
  retry: false,
237
307
  // Parameterized test metadata
308
+ example: example, // Parameters as object for API grouping
238
309
  isParameterized: isParameterized,
239
- parameters: parameters,
310
+ parameters: parameters, // Keep original array for reference
240
311
  baseMethodName: baseMethodName,
241
312
  };
242
313
  }
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 ? false : true),
18
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,7 +93,8 @@ 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
- } else this.batch.tests.push(data);
96
+ }
97
+ else this.batch.tests.push(data);
97
98
 
98
99
  if (!this.batch.intervalFunction) await this.batchUpload();
99
100
  }
@@ -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 ? false : true),
23
+ isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? 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,6 +104,7 @@ class TestomatioPipe {
104
104
  // add test ID + run ID
105
105
  if (data.rid) data.rid = `${this.runId}-${data.rid}`;
106
106
 
107
+
107
108
  if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
108
109
  data.stack = null;
109
110
  }
@@ -119,6 +120,7 @@ class TestomatioPipe {
119
120
  return data;
120
121
  }
121
122
 
123
+
122
124
  /**
123
125
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
124
126
  * @param {Object} opts - The options for preparing the test grepList.
@@ -213,7 +215,7 @@ class TestomatioPipe {
213
215
  method: 'PUT',
214
216
  url: `/api/reporter/${this.runId}`,
215
217
  data: runParams,
216
- responseType: 'json',
218
+ responseType: 'json'
217
219
  });
218
220
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
219
221
  return;
@@ -226,7 +228,7 @@ class TestomatioPipe {
226
228
  url: '/api/reporter',
227
229
  data: runParams,
228
230
  maxContentLength: Infinity,
229
- responseType: 'json',
231
+ responseType: 'json'
230
232
  });
231
233
 
232
234
  this.runId = resp.data.uid;
@@ -285,44 +287,44 @@ class TestomatioPipe {
285
287
 
286
288
  debug('Adding test', json);
287
289
 
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
- }
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: '' };
314
304
  console.log(
315
305
  APP_PREFIX,
316
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
- `Report couldn't be processed: ${err?.response?.data?.message}`,
306
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
+ pc.gray(data?.title || ''),
318
308
  );
319
- printCreateIssue(err);
320
- } else {
321
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
309
+ if (err.response?.data?.message?.includes('could not be matched')) {
310
+ this.hasUnmatchedTests = true;
311
+ }
312
+ return;
322
313
  }
323
- });
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
+ });
324
324
  };
325
325
 
326
+
327
+
326
328
  /**
327
329
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
328
330
  */
@@ -347,42 +349,43 @@ class TestomatioPipe {
347
349
  const testsToSend = this.batch.tests.splice(0);
348
350
  debug('📨 Batch upload', testsToSend.length, 'tests');
349
351
 
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
- }
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: '' };
376
370
  console.log(
377
371
  APP_PREFIX,
378
- pc.yellow(`Warning: (${err.response?.status})`),
379
- `Report couldn't be processed: ${err?.response?.data?.message}`,
372
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
380
373
  );
381
- printCreateIssue(err);
382
- } else {
383
- console.log(APP_PREFIX, "Report couldn't be processed", err);
374
+ if (err.response?.data?.message?.includes('could not be matched')) {
375
+ this.hasUnmatchedTests = true;
376
+ }
377
+ return;
384
378
  }
385
- });
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
+ });
386
389
  };
387
390
 
388
391
  /**
@@ -405,9 +408,9 @@ class TestomatioPipe {
405
408
  else this.batch.tests.push(data);
406
409
 
407
410
  // if test is added after run which is already finished
408
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
411
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
409
412
 
410
- // return promise to be able to wait for it
413
+ // return promise to be able to wait for it
411
414
  return uploading;
412
415
  }
413
416
 
@@ -456,11 +459,9 @@ class TestomatioPipe {
456
459
  status_event,
457
460
  detach: params.detach,
458
461
  tests: params.tests,
459
- },
462
+ }
460
463
  });
461
464
 
462
- debug(APP_PREFIX, '✅ Testrun finished');
463
-
464
465
  if (this.runUrl) {
465
466
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
466
467
  }
@@ -471,7 +472,7 @@ class TestomatioPipe {
471
472
  if (this.runUrl && this.proceed) {
472
473
  const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
473
474
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
474
- console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
475
+ console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
475
476
  }
476
477
 
477
478
  if (this.hasUnmatchedTests) {
@@ -524,6 +525,9 @@ function printCreateIssue(err) {
524
525
  console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
525
526
  console.log('```');
526
527
  });
528
+
527
529
  }
528
530
 
531
+
532
+
529
533
  export default TestomatioPipe;
package/src/reporter.js CHANGED
@@ -1,8 +1,10 @@
1
- // import TestomatClient from './client.js';
2
- // import * as TRConstants from './constants.js';
1
+ import Client from './client.js';
2
+ import * as TestomatioConstants from './constants.js';
3
3
  import { services } from './services/index.js';
4
4
  import reporterFunctions from './reporter-functions.js';
5
5
 
6
+ export { Client };
7
+ export const STATUS = TestomatioConstants.STATUS;
6
8
  export const artifact = reporterFunctions.artifact;
7
9
  export const log = reporterFunctions.log;
8
10
  export const logger = services.logger;
@@ -35,6 +37,7 @@ export default {
35
37
  linkTest: reporterFunctions.linkTest,
36
38
  linkJira: reporterFunctions.linkJira,
37
39
 
38
- // TestomatClient,
39
- // TRConstants,
40
+ TestomatioClient: Client,
41
+ STATUS,
42
+
40
43
  };