@testomatio/reporter 1.2.0-beta-3 → 1.2.0

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.
@@ -7,7 +7,11 @@
7
7
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
9
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer">
10
- <title>Report #{{runId}} - Testomat.io</title>
10
+ {{#if runId}}
11
+ <title>Report #{{runId}} - Testomat.io</title>
12
+ {{else}}
13
+ <title>Report Testomat.io</title>
14
+ {{/if}}
11
15
  <style>
12
16
  body {
13
17
  padding: 0;
@@ -32,7 +36,7 @@
32
36
  }
33
37
  p, span{
34
38
  font-weight: 400;
35
- font-size: 12px;
39
+ font-size: 14px;
36
40
  color: grey;
37
41
  margin: 0;
38
42
  }
@@ -245,6 +249,10 @@
245
249
  font-size: 14px;
246
250
  white-space: pre-line;
247
251
  }
252
+ .test__empty__list {
253
+ margin-bottom: 10px;
254
+ color: grey;
255
+ }
248
256
  /* Passed TAB*/
249
257
  #passedTest:not(:checked) + .btn-outline-dark {
250
258
  background-color: #39BD2F;
@@ -279,19 +287,27 @@
279
287
  <div class="col-12">
280
288
  <div class="header">
281
289
  <div class="header__block">
282
- <div class="header__case">
283
- <p>Run</p>
284
- <span><strong>#</strong>{{runId}}</span>
285
- </div>
290
+ {{#if runId}}
291
+ <div class="header__case">
292
+ <p>Run</p>
293
+ <span><strong>#</strong>{{runId}}</span>
294
+ </div>
295
+ {{/if}}
286
296
  <p class="header__type">
287
297
  <i class="fa-solid fa-face-smile"></i>
288
298
  automated job
289
299
  </p>
290
300
  </div>
291
301
  <div class="header__block">
292
- <a href="{{runUrl}}" target="_blank" class="btn btn-primary">
293
- Full Report
294
- </a>
302
+ {{#if runUrl}}
303
+ <a href="{{runUrl}}" target="_blank" class="btn btn-primary">
304
+ Full Report
305
+ </a>
306
+ {{else}}
307
+ <button class="btn btn-primary" disabled>
308
+ Full Report
309
+ </button>
310
+ {{/if}}
295
311
  </div>
296
312
  </div>
297
313
  </div>
@@ -389,72 +405,78 @@
389
405
 
390
406
  <div class="row level_5">
391
407
  <div class="col-12">
408
+ {{#if tests.length}}
409
+ <div class="testWrapp">
410
+ {{#each tests}}
392
411
 
393
- <div class="testWrapp">
394
- {{#each tests}}
412
+ <div class="testitem d-none" name="testitem" type="dummy" category="false">
395
413
 
396
- <div class="testitem d-none" name="testitem" type="dummy" category="false">
397
-
398
- <div class="testitem__top">
399
- <div class="testitem__icon">
400
- <i class="fa-solid fa-chevron-right testitem__ico testitem__ico_right"></i>
401
- <i class="fa-solid fa-chevron-down d-none testitem__ico testitem__ico_down"></i>
414
+ <div class="testitem__top">
415
+ <div class="testitem__icon">
416
+ <i class="fa-solid fa-chevron-right testitem__ico testitem__ico_right"></i>
417
+ <i class="fa-solid fa-chevron-down d-none testitem__ico testitem__ico_down"></i>
418
+ </div>
419
+ <p class="testitem__name">Test</p>
402
420
  </div>
403
- <p class="testitem__name">Test</p>
404
- </div>
405
421
 
406
- <div class="testitem__body d-none">
422
+ <div class="testitem__body d-none">
407
423
 
408
- <div class="testitem__menu">
409
- <span type="steps" class="testitem__mitem testitem__mitem_active">Steps <i class="fa-solid fa-arrow-right"></i></span>
410
- <span type="status" class="testitem__mitem">Status <i class="fa-solid fa-arrow-right"></i></span>
411
- <span type="message" class="testitem__mitem">Message <i class="fa-solid fa-arrow-right"></i></span>
412
- <span type="files" class="testitem__mitem">Files <i class="fa-solid fa-arrow-right"></i></span>
413
- </div>
424
+ <div class="testitem__menu">
425
+ <span type="steps" class="testitem__mitem testitem__mitem_active">Steps <i class="fa-solid fa-arrow-right"></i></span>
426
+ <span type="status" class="testitem__mitem">Status <i class="fa-solid fa-arrow-right"></i></span>
427
+ <span type="message" class="testitem__mitem">Message <i class="fa-solid fa-arrow-right"></i></span>
428
+ <span type="files" class="testitem__mitem">Files <i class="fa-solid fa-arrow-right"></i></span>
429
+ </div>
414
430
 
415
- <div class="testitem__content">
431
+ <div class="testitem__content">
416
432
 
417
- <!-- 1 -->
418
- <div class="testitem__case" type="steps">
419
- <p class="testitem__title">Steps</p>
420
- <div class="testitem__block">
421
- <span>...</span>
433
+ <!-- 1 -->
434
+ <div class="testitem__case" type="steps">
435
+ <p class="testitem__title">Steps</p>
436
+ <div class="testitem__block">
437
+ <span>...</span>
438
+ </div>
422
439
  </div>
423
- </div>
424
- <!-- 1 -->
425
-
426
- <!-- 2 -->
427
- <div class="testitem__case d-none" type="status">
428
- <p class="testitem__title">Status</p>
429
- <div class="testitem__block">
430
- <span>...</span>
440
+ <!-- 1 -->
441
+
442
+ <!-- 2 -->
443
+ <div class="testitem__case d-none" type="status">
444
+ <p class="testitem__title">Status</p>
445
+ <div class="testitem__block">
446
+ <span>...</span>
447
+ </div>
431
448
  </div>
432
- </div>
433
- <!-- 2 -->
434
-
435
- <!-- 3 -->
436
- <div class="testitem__case d-none" type="message">
437
- <p class="testitem__title">Message</p>
438
- <div class="testitem__block">
439
- <span>...</span>
449
+ <!-- 2 -->
450
+
451
+ <!-- 3 -->
452
+ <div class="testitem__case d-none" type="message">
453
+ <p class="testitem__title">Message</p>
454
+ <div class="testitem__block">
455
+ <span>...</span>
456
+ </div>
440
457
  </div>
441
- </div>
442
- <!-- 3 -->
443
-
444
- <!-- 4 -->
445
- <div class="testitem__case d-none" type="files">
446
- <p class="testitem__title">Files</p>
447
- <div class="testitem__block">
448
- <span>...</span>
458
+ <!-- 3 -->
459
+
460
+ <!-- 4 -->
461
+ <div class="testitem__case d-none" type="files">
462
+ <p class="testitem__title">Files</p>
463
+ <div class="testitem__block">
464
+ <span>...</span>
465
+ </div>
449
466
  </div>
467
+ <!-- 4 -->
450
468
  </div>
451
- <!-- 4 -->
452
469
  </div>
453
470
  </div>
471
+ {{/each}}
472
+ </div>
473
+ {{else}}
474
+ <div class="row">
475
+ <div class="col-12 test__empty__list">
476
+ <p>No tests found to display 😔</p>
454
477
  </div>
455
- {{/each}}
456
- </div>
457
-
478
+ </div>
479
+ {{/if}}
458
480
  </div>
459
481
  </div>
460
482
 
@@ -469,23 +491,33 @@
469
491
 
470
492
  // Draw the chart and set the chart values
471
493
  function drawChart() {
494
+ let data = {};
472
495
  const passedTests = {{getTestsByStatus tests "PASSED"}};
473
496
  const failedTests = {{getTestsByStatus tests "FAILED"}};
474
497
  const skippedTests = {{getTestsByStatus tests "SKIPPED"}};
475
498
 
476
- const data = google.visualization.arrayToDataTable([
499
+ if (passedTests === 0 && failedTests === 0 && skippedTests === 0) {
500
+ data = google.visualization.arrayToDataTable([
501
+ ['Task', 'Tests'],
502
+ ['No Tests', 1]
503
+ ])
504
+ }
505
+ else {
506
+ data = google.visualization.arrayToDataTable([
477
507
  ['Task', 'Tests'],
508
+ ['No Tests', 0],
478
509
  ['Passed', passedTests],
479
510
  ['Failed', failedTests],
480
511
  ['Skipped', skippedTests],
481
512
  ]);
513
+ }
482
514
 
483
515
  // Optional: add a title and set the width and height of the chart
484
516
  const options = {
485
517
  'title':'',
486
518
  'width':550,
487
519
  'height':300,
488
- 'colors': ['#39BD2F', '#F2230C', '#F2C00C']
520
+ 'colors': ['#939992', '#39BD2F', '#F2230C', '#F2C00C']
489
521
  };
490
522
 
491
523
  // Display the chart inside the <div> element with id="piechart"
@@ -141,7 +141,15 @@ const fetchSourceCode = (contents, opts = {}) => {
141
141
  // remove special chars from title
142
142
  if (!lineIndex && opts.title) {
143
143
  const title = opts.title.replace(/[([@].*/g, '');
144
- lineIndex = lines.findIndex(l => l.includes(title));
144
+
145
+ if (opts.lang === 'java') {
146
+ lineIndex = lines.findIndex(l => l.includes(`test${title}`));
147
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
148
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
149
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
150
+ } else {
151
+ lineIndex = lines.findIndex(l => l.includes(title));
152
+ }
145
153
  }
146
154
 
147
155
  if (opts.prepend) {
@@ -290,6 +298,23 @@ function removeColorCodes(input) {
290
298
  return input.replace(/\x1b\[[0-9;]*m/g, '');
291
299
  }
292
300
 
301
+ const testRunnerHelper = {
302
+ // for Jest
303
+ getNameOfCurrentlyRunningTest: () => {
304
+ if (global.testomatioTestTitle) return global.testomatioTestTitle;
305
+
306
+ if (!process.env.JEST_WORKER_ID) return null;
307
+ try {
308
+ // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
309
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
310
+ // eslint-disable-next-line no-undef
311
+ return expect?.getState()?.currentTestName;
312
+ } catch (e) {
313
+ return null;
314
+ }
315
+ },
316
+ };
317
+
293
318
  module.exports = {
294
319
  isSameTest,
295
320
  fetchSourceCode,
@@ -306,5 +331,6 @@ module.exports = {
306
331
  parseSuite,
307
332
  humanize,
308
333
  removeColorCodes,
309
- foundedTestLog
334
+ foundedTestLog,
335
+ testRunnerHelper,
310
336
  };
package/lib/xmlReader.js CHANGED
@@ -1,57 +1,57 @@
1
1
  const debug = require('debug')('@testomatio/reporter:xml');
2
- const path = require("path");
2
+ const path = require('path');
3
3
  const chalk = require('chalk');
4
- const fs = require("fs");
5
- const { XMLParser } = require("fast-xml-parser");
4
+ const fs = require('fs');
5
+ const { XMLParser } = require('fast-xml-parser');
6
6
  const { APP_PREFIX, STATUS } = require('./constants');
7
- const { fetchFilesFromStackTrace,
8
- fetchIdFromOutput,
9
- fetchSourceCode,
10
- fetchSourceCodeFromStackTrace,
11
- fetchIdFromCode,
12
- humanize
13
- } = require('./utils/utils');
7
+ const {
8
+ fetchFilesFromStackTrace,
9
+ fetchIdFromOutput,
10
+ fetchSourceCode,
11
+ fetchSourceCodeFromStackTrace,
12
+ fetchIdFromCode,
13
+ humanize,
14
+ } = require('./utils/utils');
14
15
  const upload = require('./fileUploader');
15
16
  const pipesFactory = require('./pipe');
16
17
  const adapterFactory = require('./junit-adapter');
18
+ const config = require('./config');
17
19
 
18
- const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
19
- const TESTOMATIO = process.env.TESTOMATIO; // key?
20
+ const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
20
21
  const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
21
22
 
22
23
  const options = {
23
24
  ignoreDeclaration: true,
24
25
  ignoreAttributes: false,
25
26
  alwaysCreateTextNode: false,
26
- attributeNamePrefix: "",
27
+ attributeNamePrefix: '',
27
28
  parseTagValue: true,
28
29
  };
29
30
 
30
31
  const reduceOptions = {};
31
32
 
32
33
  class XmlReader {
33
-
34
34
  constructor(opts = {}) {
35
35
  this.requestParams = {
36
- apiKey: opts.apiKey || TESTOMATIO,
36
+ apiKey: opts.apiKey || config.TESTOMATIO,
37
37
  url: opts.url || TESTOMATIO_URL,
38
38
  title: TESTOMATIO_TITLE,
39
39
  env: TESTOMATIO_ENV,
40
40
  group_title: TESTOMATIO_RUNGROUP_TITLE,
41
41
  };
42
42
  this.runId = opts.runId || TESTOMATIO_RUN;
43
- this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
43
+ this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts);
44
44
  if (!this.adapter) throw new Error('XML adapter for this format not found');
45
45
 
46
46
  this.opts = opts || {};
47
- this.store = {}
47
+ this.store = {};
48
48
  this.pipes = pipesFactory(opts, this.store);
49
49
 
50
50
  this.parser = new XMLParser(options);
51
- this.tests = []
52
- this.stats = {}
51
+ this.tests = [];
52
+ this.stats = {};
53
53
  this.stats.language = opts.lang?.toLowerCase();
54
- this.filesToUpload = {}
54
+ this.filesToUpload = {};
55
55
 
56
56
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
57
57
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -82,8 +82,8 @@ class XmlReader {
82
82
  } else if (jsonResult.assemblies) {
83
83
  return this.processXUnit(jsonResult.assemblies);
84
84
  } else {
85
- console.log(jsonResult)
86
- throw new Error("Format can't be parsed")
85
+ console.log(jsonResult);
86
+ throw new Error("Format can't be parsed");
87
87
  }
88
88
 
89
89
  return this.processJUnit(jsonSuite);
@@ -96,7 +96,7 @@ class XmlReader {
96
96
  const resultTests = processTestSuite(testsuite);
97
97
 
98
98
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
99
- const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
99
+ const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
100
100
 
101
101
  this.tests = this.tests.concat(resultTests);
102
102
 
@@ -135,21 +135,22 @@ class XmlReader {
135
135
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
136
136
  if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
137
137
 
138
- const tests = defs.map(td => {
139
- const title = td.name.replace(/\(.*?\)/, '').trim();
140
- let example = td.name.match(/\((.*?)\)/);
141
- if (example) example = { ...example[1].split(',') };
142
- const suite = td.TestMethod.className.split(', ')[0].split('.');
143
- const suite_title = suite.pop();
144
- return {
145
- title,
146
- example,
147
- file: suite.join('/'),
148
- description: td.Description,
149
- suite_title,
150
- id: td.Execution.id,
151
- }
152
- }) || [];
138
+ const tests =
139
+ defs.map(td => {
140
+ const title = td.name.replace(/\(.*?\)/, '').trim();
141
+ let example = td.name.match(/\((.*?)\)/);
142
+ if (example) example = { ...example[1].split(',') };
143
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
144
+ const suite_title = suite.pop();
145
+ return {
146
+ title,
147
+ example,
148
+ file: suite.join('/'),
149
+ description: td.Description,
150
+ suite_title,
151
+ id: td.Execution.id,
152
+ };
153
+ }) || [];
153
154
 
154
155
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
155
156
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
@@ -160,10 +161,9 @@ class XmlReader {
160
161
  run_time: parseFloat(td.duration) * 1000,
161
162
  status: td.outcome,
162
163
  stack: td.Output.StdOut,
163
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
164
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
164
165
  }));
165
166
 
166
-
167
167
  results.forEach(r => {
168
168
  const test = tests.find(t => t.id === r.id) || {};
169
169
  r.suite_title = test.suite_title;
@@ -223,7 +223,7 @@ class XmlReader {
223
223
 
224
224
  if (testCase.failure) {
225
225
  message = testCase.failure.message;
226
- stack = testCase.failure['stack-trace']
226
+ stack = testCase.failure['stack-trace'];
227
227
  }
228
228
  if (testCase.reason) {
229
229
  message = testCase.reason.message;
@@ -250,7 +250,6 @@ class XmlReader {
250
250
  suite_title,
251
251
  run_time,
252
252
  });
253
-
254
253
  });
255
254
  });
256
255
  });
@@ -283,12 +282,12 @@ class XmlReader {
283
282
  passed_count: 0,
284
283
  failed_count: 0,
285
284
  skipped_count: 0,
286
- }
285
+ };
287
286
  this.tests.forEach(t => {
288
287
  this.stats.tests_count++;
289
288
  if (t.status === 'passed') this.stats.passed_count++;
290
289
  if (t.status === 'failed') this.stats.failed_count++;
291
- })
290
+ });
292
291
  if (this.stats.failed_count) this.stats.status = 'failed';
293
292
 
294
293
  return this.stats;
@@ -297,7 +296,7 @@ class XmlReader {
297
296
  fetchSourceCode() {
298
297
  this.tests.forEach(t => {
299
298
  try {
300
- const file = this.adapter.getFilePath(t)
299
+ const file = this.adapter.getFilePath(t);
301
300
  if (!file) return;
302
301
 
303
302
  if (!this.stats.language) {
@@ -310,16 +309,16 @@ class XmlReader {
310
309
  }
311
310
 
312
311
  if (!fs.existsSync(file)) {
313
- debug('Failed to open file with the source code', file)
312
+ debug('Failed to open file with the source code', file);
314
313
  return;
315
314
  }
316
315
  const contents = fs.readFileSync(file).toString();
317
- t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
316
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language });
318
317
  if (t.code) debug('Fetched code for test %s', t.title);
319
- t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
318
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language });
320
319
  if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
321
320
  } catch (err) {
322
- debug(err)
321
+ debug(err);
323
322
  }
324
323
  });
325
324
  }
@@ -327,21 +326,22 @@ class XmlReader {
327
326
  formatTests() {
328
327
  this.tests.forEach(t => {
329
328
  if (t.file) {
330
- t.file = t.file.replace(process.cwd() + path.sep, '')
329
+ t.file = t.file.replace(process.cwd() + path.sep, '');
331
330
  }
332
331
 
333
- this.adapter.formatTest(t)
332
+ this.adapter.formatTest(t);
334
333
 
335
334
  t.title = humanize(t.title);
336
335
  });
337
336
  }
338
337
 
339
338
  formatErrors() {
340
- this.tests.filter(t => !!t.stack).forEach(t => {
341
- t.stack = this.formatStack(t)
342
- t.message = this.adapter.formatMessage(t);
343
- });
344
-
339
+ this.tests
340
+ .filter(t => !!t.stack)
341
+ .forEach(t => {
342
+ t.stack = this.formatStack(t);
343
+ t.message = this.adapter.formatMessage(t);
344
+ });
345
345
  }
346
346
 
347
347
  formatStack(t) {
@@ -359,7 +359,7 @@ class XmlReader {
359
359
  async uploadArtifacts() {
360
360
  for (const test of this.tests.filter(t => !!t.stack)) {
361
361
  let files = [];
362
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
362
+ if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
363
363
  files = [...files, ...fetchFilesFromStackTrace(test.stack)];
364
364
 
365
365
  if (!files.length) continue;
@@ -378,7 +378,7 @@ class XmlReader {
378
378
  group_title: this.requestParams.group_title,
379
379
  };
380
380
 
381
- debug("Run", runParams);
381
+ debug('Run', runParams);
382
382
 
383
383
  return Promise.all(this.pipes.map(p => p.createRun(runParams)));
384
384
  }
@@ -391,12 +391,10 @@ class XmlReader {
391
391
  this.formatErrors();
392
392
  this.formatTests();
393
393
 
394
- debug(
395
- 'Uploading data',
396
- {
397
- ...this.stats,
398
- tests: this.tests,
399
- })
394
+ debug('Uploading data', {
395
+ ...this.stats,
396
+ tests: this.tests,
397
+ });
400
398
 
401
399
  const dataString = {
402
400
  ...this.stats,
@@ -411,59 +409,62 @@ class XmlReader {
411
409
 
412
410
  module.exports = XmlReader;
413
411
 
414
-
415
412
  function reduceTestCases(prev, item) {
416
413
  let testCases = item.testcase;
417
414
  if (!testCases) testCases = item['test-case'];
418
415
  if (!Array.isArray(testCases)) {
419
- testCases = [testCases]
416
+ testCases = [testCases];
420
417
  }
421
418
  const suiteOutput = item['system-out'] || item.output || item.log || '';
422
419
  const suiteErr = item['system-err'] || item.output || item.log || '';
423
- testCases.filter(t => !!t).forEach(testCaseItem => {
424
- const file = testCaseItem.file || item.filepath || '';
425
-
426
- let stack = '';
427
- let message = '';
428
- if (testCaseItem.error) stack = testCaseItem.error;
429
- if (testCaseItem.failure) stack = testCaseItem.failure;
430
- if (testCaseItem?.failure?.['stack-trace']) stack = testCaseItem.failure['stack-trace'];
431
- if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
432
- if (testCaseItem?.error?.message) message = testCaseItem.error.message;
433
-
434
- if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
435
- if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
436
- if (!message) message = stack.trim().split('\n')[0];
437
-
438
- // eslint-disable-next-line
439
- stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
440
- const testId = fetchIdFromOutput(stack);
441
-
442
- let status = STATUS.PASSED.toString();
443
- if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
444
- if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
445
-
446
- prev.push({
447
- create: true,
448
- file,
449
- stack,
450
- test_id: testId,
451
- message,
452
- line: testCaseItem.lineno,
453
- // seconds are used in junit reports, but ms are used by testomatio
454
- run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
455
- status,
456
- title: testCaseItem.name,
457
- suite_title: reduceOptions.preferClassname ? testCaseItem.classname : (item.name || testCaseItem.classname),
458
- })
459
- });
420
+ testCases
421
+ .filter(t => !!t)
422
+ .forEach(testCaseItem => {
423
+ const file = testCaseItem.file || item.filepath || '';
424
+
425
+ let stack = '';
426
+ let message = '';
427
+ if (testCaseItem.error) stack = testCaseItem.error;
428
+ if (testCaseItem.failure) stack = testCaseItem.failure;
429
+ if (testCaseItem?.failure?.['stack-trace']) stack = testCaseItem.failure['stack-trace'];
430
+ if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
431
+ if (testCaseItem?.error?.message) message = testCaseItem.error.message;
432
+
433
+ if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
434
+ if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
435
+ if (!message) message = stack.trim().split('\n')[0];
436
+
437
+ // eslint-disable-next-line
438
+ stack = `${
439
+ testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
440
+ }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
441
+ const testId = fetchIdFromOutput(stack);
442
+
443
+ let status = STATUS.PASSED.toString();
444
+ if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
445
+ if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
446
+
447
+ prev.push({
448
+ create: true,
449
+ file,
450
+ stack,
451
+ test_id: testId,
452
+ message,
453
+ line: testCaseItem.lineno,
454
+ // seconds are used in junit reports, but ms are used by testomatio
455
+ run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
456
+ status,
457
+ title: testCaseItem.name,
458
+ suite_title: reduceOptions.preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname,
459
+ });
460
+ });
460
461
  return prev;
461
462
  }
462
463
 
463
464
  function processTestSuite(testsuite) {
464
465
  if (!testsuite) return [];
465
- if (testsuite.testsuite) return processTestSuite(testsuite.testsuite)
466
- if (testsuite['test-suite']) return processTestSuite(testsuite['test-suite'])
466
+ if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
467
+ if (testsuite['test-suite']) return processTestSuite(testsuite['test-suite']);
467
468
 
468
469
  let suites = testsuite;
469
470
  if (!Array.isArray(testsuite)) {