@testomatio/reporter 2.0.1-beta-ignore-xml → 2.0.1-beta.1

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.
Files changed (63) hide show
  1. package/lib/adapter/codecept.js +0 -2
  2. package/lib/adapter/cypress-plugin/index.js +0 -2
  3. package/lib/adapter/mocha.js +0 -1
  4. package/lib/adapter/nightwatch.d.ts +4 -0
  5. package/lib/adapter/nightwatch.js +80 -0
  6. package/lib/adapter/webdriver.d.ts +1 -1
  7. package/lib/adapter/webdriver.js +17 -8
  8. package/lib/bin/cli.js +126 -8
  9. package/lib/bin/reportXml.js +4 -2
  10. package/lib/bin/startTest.js +3 -2
  11. package/lib/bin/uploadArtifacts.js +5 -4
  12. package/lib/client.js +18 -9
  13. package/lib/config.js +2 -2
  14. package/lib/data-storage.d.ts +1 -1
  15. package/lib/data-storage.js +17 -7
  16. package/lib/junit-adapter/csharp.d.ts +1 -0
  17. package/lib/junit-adapter/csharp.js +11 -1
  18. package/lib/pipe/bitbucket.d.ts +2 -0
  19. package/lib/pipe/bitbucket.js +38 -26
  20. package/lib/pipe/debug.js +17 -3
  21. package/lib/pipe/github.d.ts +2 -2
  22. package/lib/pipe/github.js +35 -3
  23. package/lib/pipe/gitlab.d.ts +2 -0
  24. package/lib/pipe/gitlab.js +27 -9
  25. package/lib/pipe/html.d.ts +1 -0
  26. package/lib/pipe/html.js +1 -3
  27. package/lib/pipe/index.js +17 -7
  28. package/lib/pipe/testomatio.d.ts +2 -1
  29. package/lib/pipe/testomatio.js +79 -73
  30. package/lib/reporter.d.ts +12 -12
  31. package/lib/services/artifacts.d.ts +1 -1
  32. package/lib/services/key-values.d.ts +1 -1
  33. package/lib/services/logger.d.ts +1 -1
  34. package/lib/services/logger.js +1 -2
  35. package/lib/template/testomatio.hbs +443 -68
  36. package/lib/uploader.js +2 -2
  37. package/lib/utils/utils.d.ts +2 -0
  38. package/lib/utils/utils.js +41 -18
  39. package/lib/xmlReader.js +54 -19
  40. package/package.json +8 -9
  41. package/src/adapter/codecept.js +0 -2
  42. package/src/adapter/cypress-plugin/index.js +0 -2
  43. package/src/adapter/mocha.js +0 -1
  44. package/src/adapter/nightwatch.js +88 -0
  45. package/src/adapter/webdriver.js +1 -2
  46. package/src/bin/cli.js +131 -2
  47. package/src/bin/reportXml.js +4 -1
  48. package/src/bin/startTest.js +2 -1
  49. package/src/bin/uploadArtifacts.js +2 -1
  50. package/src/client.js +1 -2
  51. package/src/config.js +2 -2
  52. package/src/junit-adapter/csharp.js +13 -1
  53. package/src/pipe/bitbucket.js +22 -24
  54. package/src/pipe/debug.js +18 -3
  55. package/src/pipe/github.js +1 -2
  56. package/src/pipe/gitlab.js +27 -9
  57. package/src/pipe/html.js +3 -4
  58. package/src/pipe/testomatio.js +98 -103
  59. package/src/services/logger.js +1 -2
  60. package/src/template/testomatio.hbs +443 -68
  61. package/src/uploader.js +2 -2
  62. package/src/utils/utils.js +19 -9
  63. package/src/xmlReader.js +69 -17
package/lib/xmlReader.js CHANGED
@@ -20,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
20
20
  const debug = (0, debug_1.default)('@testomatio/reporter:xml');
21
21
  const ridRunId = (0, crypto_1.randomUUID)();
22
22
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
23
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
23
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
24
24
  const options = {
25
25
  ignoreDeclaration: true,
26
26
  ignoreAttributes: false,
@@ -28,6 +28,7 @@ const options = {
28
28
  attributeNamePrefix: '',
29
29
  parseTagValue: true,
30
30
  };
31
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
31
32
  const reduceOptions = {};
32
33
  class XmlReader {
33
34
  constructor(opts = {}) {
@@ -75,7 +76,7 @@ class XmlReader {
75
76
  /(<system-out><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-out>)/g,
76
77
  ];
77
78
  for (const regex of cutRegexes) {
78
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
79
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
79
80
  }
80
81
  const jsonResult = this.parser.parse(xmlData);
81
82
  let jsonSuite;
@@ -202,7 +203,7 @@ class XmlReader {
202
203
  this.tests = results.filter(t => !!t.title);
203
204
  return {
204
205
  status,
205
- create_tests: true,
206
+ create_tests: !process.env.IGNORE_NEW_TESTS,
206
207
  tests_count: parseInt(counters.total, 10),
207
208
  passed_count: parseInt(counters.passed, 10),
208
209
  skipped_count: parseInt(counters.notExecuted, 10),
@@ -313,6 +314,8 @@ class XmlReader {
313
314
  this.stats.language = 'js';
314
315
  if (file.endsWith('.ts'))
315
316
  this.stats.language = 'ts';
317
+ if (file.endsWith('.cs'))
318
+ this.stats.language = 'csharp';
316
319
  }
317
320
  if (!fs_1.default.existsSync(file)) {
318
321
  debug('Failed to open file with the source code', file);
@@ -359,13 +362,13 @@ class XmlReader {
359
362
  async uploadArtifacts() {
360
363
  for (const test of this.tests.filter(t => !!t.stack)) {
361
364
  let files = [];
362
- if (test.files?.length)
363
- files = test.files.map(f => path_1.default.join(process.cwd(), f));
364
- files = [...files, ...(0, utils_js_1.fetchFilesFromStackTrace)(test.stack)];
365
+ if (!test.files?.length)
366
+ continue;
367
+ files = test.files.map(f => (path_1.default.isAbsolute(f) ? f : path_1.default.join(process.cwd(), f)));
365
368
  if (!files.length)
366
369
  continue;
367
370
  const runId = this.runId || this.store.runId || Date.now().toString();
368
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
371
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path_1.default.basename(f)])));
369
372
  console.log(constants_js_1.APP_PREFIX, `🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
370
373
  }
371
374
  }
@@ -415,14 +418,17 @@ function reduceTestCases(prev, item) {
415
418
  testCases = [testCases];
416
419
  }
417
420
  // suite inside test case
418
- if (item['test-suite'] && item['test-suite']['test-case'])
419
- testCases.push(...item['test-suite']['test-case']);
421
+ const testCase = item['test-suite']?.['test-case'];
422
+ if (testCase) {
423
+ const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
424
+ testCases.push(...nestedCases);
425
+ }
420
426
  const suiteOutput = item['system-out'] || item.output || item.log || '';
421
427
  const suiteErr = item['system-err'] || item.output || item.log || '';
422
428
  testCases
423
429
  .filter(t => !!t)
424
430
  .forEach(testCaseItem => {
425
- const file = testCaseItem.file || item.filepath || '';
431
+ const file = testCaseItem.file || item.filepath || item.fullname || '';
426
432
  let stack = '';
427
433
  let message = '';
428
434
  if (testCaseItem.error)
@@ -444,7 +450,7 @@ function reduceTestCases(prev, item) {
444
450
  const isParametrized = item.type === 'ParameterizedMethod';
445
451
  const preferClassname = reduceOptions.preferClassname || isParametrized;
446
452
  // SpecFlow config
447
- let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
453
+ let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
448
454
  let example = null;
449
455
  const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
450
456
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
@@ -454,17 +460,38 @@ function reduceTestCases(prev, item) {
454
460
  example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
455
461
  title = title.replace(/\(.*?\)/, '').trim();
456
462
  }
457
- // eslint-disable-next-line
458
463
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
459
- const testId = (0, utils_js_1.fetchIdFromOutput)(stack);
464
+ if (!testId)
465
+ testId = (0, utils_js_1.fetchIdFromOutput)(stack);
466
+ if (tags?.length && !testId) {
467
+ testId = tags
468
+ .filter(t => t.startsWith('T'))
469
+ .map(t => `@${t}`)
470
+ .find(t => t.match(utils_js_1.TEST_ID_REGEX))
471
+ ?.slice(2);
472
+ }
460
473
  let status = constants_js_1.STATUS.PASSED.toString();
461
474
  if ('failure' in testCaseItem || 'error' in testCaseItem)
462
475
  status = constants_js_1.STATUS.FAILED;
463
476
  if ('skipped' in testCaseItem)
464
477
  status = constants_js_1.STATUS.SKIPPED;
478
+ if (testCaseItem.result && Object.values(constants_js_1.STATUS).includes(testCaseItem.result.toLowerCase())) {
479
+ status = testCaseItem.result.toLowerCase();
480
+ }
465
481
  let rid = null;
466
482
  if (testCaseItem.id)
467
483
  rid = `${ridRunId}-${testCaseItem.id}`;
484
+ // Extract attachments
485
+ let files = [];
486
+ if (testCaseItem.attachments) {
487
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
488
+ ? testCaseItem.attachments.attachment
489
+ : [testCaseItem.attachments.attachment];
490
+ files = attachments.filter(a => a && a.filePath).map(a => a.filePath);
491
+ }
492
+ // Extract files from stack trace using existing utility
493
+ const stackFiles = (0, utils_js_1.fetchFilesFromStackTrace)(stack);
494
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
468
495
  prev.push({
469
496
  rid,
470
497
  file,
@@ -479,7 +506,9 @@ function reduceTestCases(prev, item) {
479
506
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
480
507
  status,
481
508
  title,
509
+ root_suite_id: TESTOMATIO_SUITE,
482
510
  suite_title: suiteTitle,
511
+ files,
483
512
  });
484
513
  });
485
514
  return prev;
@@ -503,12 +532,18 @@ function fetchProperties(item) {
503
532
  let title = '';
504
533
  if (!item.properties)
505
534
  return {};
506
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
535
+ // Handle both single property and array of properties
536
+ const properties = Array.isArray(item.properties.property)
537
+ ? item.properties.property
538
+ : [item.properties.property].filter(Boolean);
539
+ const prop = properties.find(p => p.name === 'Description');
507
540
  if (prop)
508
541
  title = prop.value;
509
- [item.properties?.property]
510
- .flat()
511
- .filter(p => p.name === 'Category')
512
- .forEach(p => tags.push(p.value));
513
- return { title, tags };
542
+ let testId = properties.find(p => p.name === 'ID')?.value;
543
+ if (testId?.startsWith('@'))
544
+ testId = testId.slice(1);
545
+ if (testId?.startsWith('T'))
546
+ testId = testId.slice(1);
547
+ properties.filter(p => p.name === 'Category').forEach(p => tags.push(p.value));
548
+ return { title, tags, testId };
514
549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.0.1-beta-ignore-xml",
3
+ "version": "2.0.1-beta.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -14,10 +14,9 @@
14
14
  "@aws-sdk/client-s3": "^3.279.0",
15
15
  "@aws-sdk/lib-storage": "^3.279.0",
16
16
  "@cucumber/cucumber": "^10.9.0",
17
- "@octokit/rest": "^19.0.5",
17
+ "@octokit/rest": "^21.1.1",
18
18
  "aws-sdk": "^2.1072.0",
19
- "axios": "^1.6.2",
20
- "axios-retry": "^3.9.1",
19
+ "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
21
20
  "callsite-record": "^4.1.4",
22
21
  "commander": "^12",
23
22
  "cross-spawn": "^7.0.3",
@@ -49,7 +48,6 @@
49
48
  "testcafe"
50
49
  ],
51
50
  "scripts": {
52
- "@cucumber/cucumber": "^10.9.0",
53
51
  "clear-exportdir": "rm -rf export/",
54
52
  "pretty": "npx prettier --check .",
55
53
  "pretty:fix": "prettier --write .",
@@ -68,8 +66,9 @@
68
66
  "test:adapter:playwright:example": "npx playwright test --config='./tests/adapter/examples/playwright/playwright.config.ts'",
69
67
  "test:adapter:vitest:example": "npx vitest --config='./tests/adapter/examples/vitest/vitest.config.ts'",
70
68
  "test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/logger-2.test.js && npx mocha tests-storage/reporter-functions.test.js",
71
- "//": "builds code from /src (esm) into /lib (commonjs)",
72
- "build": "rm -rf ./cjs && tsc --module commonjs && npx tsx build/scripts/edit-js-files.js && npx tsx build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh"
69
+ "build": "rm -rf ./cjs && tsc --module commonjs && npx tsx build/scripts/edit-js-files.js && npx tsx build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh",
70
+ "build:bun": "rm -rf ./cjs && bun build ./src/reporter.js ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node && bun build/scripts/edit-js-files.js && bun build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh",
71
+ "build:watch:bun": "rm -rf ./cjs && bun build ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node --watch --onSuccess \"build/scripts/post-build.js\""
73
72
  },
74
73
  "devDependencies": {
75
74
  "@playwright/test": "^1.49.1",
@@ -81,8 +80,7 @@
81
80
  "chai": "^4.3.6",
82
81
  "codeceptjs": "^3.6.5",
83
82
  "cucumber": "^6.0.7",
84
- "eslint": "^8.57.0",
85
- "eslint-config-airbnb-base": "^15.0.0",
83
+ "eslint": "^9.24.0",
86
84
  "eslint-config-prettier": "^8.3.0",
87
85
  "eslint-plugin-import": "^2.25.4",
88
86
  "jasmine": "^5.2.0",
@@ -123,6 +121,7 @@
123
121
  "./lib/adapter/mocha/mocha.js": "./lib/adapter/mocha.js",
124
122
  "./mocha": "./lib/adapter/mocha.js",
125
123
  "./lib/adapter/playwright.js": "./lib/adapter/playwright.js",
124
+ "./nightwatch": "./lib/adapter/nightwatch.js",
126
125
  "./playwright": "./lib/adapter/playwright.js",
127
126
  "./vitest": "./lib/adapter/vitest.js",
128
127
  "./lib/adapter/webdriver.js": "./lib/adapter/webdriver.js",
@@ -4,7 +4,6 @@ import TestomatClient from '../client.js';
4
4
  import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
5
5
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
6
6
  import { services } from '../services/index.js';
7
- // eslint-disable-next-line
8
7
  import codeceptjs from 'codeceptjs';
9
8
 
10
9
  const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
@@ -271,7 +270,6 @@ function CodeceptReporter(config) {
271
270
  for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
272
271
  if (currentMetaStep[i] !== metaSteps[i]) {
273
272
  stepShift = 2 * i;
274
- // eslint-disable-next-line no-continue
275
273
  if (!metaSteps[i]) continue;
276
274
  if (metaSteps[i].isBDD()) {
277
275
  // output.push(repeat(stepShift) + pc.bold(metaSteps[i].toString()) + metaSteps[i].comment);
@@ -44,7 +44,6 @@ const testomatioReporter = on => {
44
44
 
45
45
  if (!error && test.displayError) {
46
46
  error = { message: test.displayError };
47
- // eslint-disable-next-line
48
47
  error.inspect = function () {
49
48
  return this.message;
50
49
  };
@@ -56,7 +55,6 @@ const testomatioReporter = on => {
56
55
  name: error.name,
57
56
  inspect:
58
57
  error.inspect ||
59
- // eslint-disable-next-line
60
58
  function () {
61
59
  return this.message;
62
60
  },
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
1
  import Mocha from 'mocha';
3
2
  import TestomatClient from '../client.js';
4
3
  import { STATUS, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
@@ -0,0 +1,88 @@
1
+ import TestomatClient from '../client.js';
2
+ import { config } from '../config.js';
3
+ import { STATUS } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
+
6
+ const apiKey = config.TESTOMATIO;
7
+ const client = new TestomatClient({ apiKey });
8
+
9
+ export default {
10
+ write: async (results, options, done) => {
11
+ await client.createRun();
12
+
13
+ const testFiles = results.modules;
14
+
15
+ for (const fileName in testFiles) {
16
+ // in nightwatch: object containing tests from a single file
17
+ const testModule = testFiles[fileName];
18
+
19
+ // passed and failed tests (tests with assertions)
20
+ const completedTests = testModule.completed;
21
+
22
+ // skipped tests (skipped by user or tests without assertions)
23
+ const skippedTests = testModule.skipped;
24
+
25
+ const tags = testModule.tags || [];
26
+
27
+ // if test file contains multiple suites, the last suite name is used as a name 🤷‍♂️
28
+ // no other places which contain suite name (even inside test object)
29
+ const suiteTitle = testModule.name;
30
+
31
+ for (const testTitle in completedTests) {
32
+ const test = completedTests[testTitle];
33
+ let status;
34
+ switch (test.status) {
35
+ case 'pass':
36
+ status = STATUS.PASSED;
37
+ break;
38
+ case 'fail':
39
+ status = STATUS.FAILED;
40
+ break;
41
+ // probably not required (because skipped tests are in separate array), but just in case
42
+ case 'skip':
43
+ status = STATUS.SKIPPED;
44
+ console.info('Skipped test is in completed tests array:', test, 'Not expected behavior.');
45
+ break;
46
+ default:
47
+ console.error('Test status processing error:', test.status);
48
+ }
49
+
50
+ const testId = getTestomatIdFromTestTitle(testTitle);
51
+
52
+ client.addTestRun(status, {
53
+ error: { name: test.assertions?.[0]?.name, message: test.assertions?.[0]?.message, stack: test.stackTrace },
54
+ file: testModule.modulePath?.replace(process.cwd(), ''),
55
+ message: test.assertions?.[0]?.message,
56
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
57
+ stack: test.stackTrace,
58
+ suite_title: suiteTitle,
59
+ tags,
60
+ test_id: testId,
61
+ time: test.timeMs,
62
+ title: testTitle,
63
+ });
64
+ }
65
+
66
+ // just array with skipped tests titles, no any other info
67
+ for (const testTitle of skippedTests) {
68
+ client.addTestRun(STATUS.SKIPPED, {
69
+ suite_title: suiteTitle,
70
+ tags,
71
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
72
+ title: testTitle,
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @type {'passed' | 'failed' | 'finished'}
79
+ */
80
+ let runStatus = 'finished';
81
+ if (results.failed) runStatus = 'failed';
82
+ else if (results.passed) runStatus = 'passed';
83
+
84
+ await client.updateRunStatus(runStatus);
85
+
86
+ done();
87
+ },
88
+ };
@@ -1,5 +1,4 @@
1
- // eslint-disable-next-line
2
- import WDIOReporter, { RunnerStats } from '@wdio/reporter';
1
+ import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
3
2
  import TestomatClient from '../client.js';
4
3
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
5
4
  import { services } from '../services/index.js';
package/src/bin/cli.js CHANGED
@@ -2,19 +2,23 @@
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import { spawn } from 'cross-spawn';
5
- import glob from 'glob';
5
+ import { glob } from 'glob';
6
6
  import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
9
  import { APP_PREFIX, STATUS } from '../constants.js';
10
- import { version } from '../../package.json';
10
+ import { getPackageVersion } from '../utils/utils.js';
11
11
  import { config } from '../config.js';
12
12
  import { readLatestRunId } from '../utils/utils.js';
13
13
  import pc from 'picocolors';
14
14
  import { filesize as prettyBytes } from 'filesize';
15
15
  import dotenv from 'dotenv';
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
16
19
 
17
20
  const debug = createDebugMessages('@testomatio/reporter:xml-cli');
21
+ const version = getPackageVersion();
18
22
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
19
23
  const program = new Command();
20
24
 
@@ -120,6 +124,28 @@ program
120
124
  }
121
125
  });
122
126
 
127
+ // program
128
+ // .command('xml')
129
+ // .description('Parse XML reports and upload to Testomat.io')
130
+ // .argument('<pattern>', 'XML file pattern')
131
+ // .option('-d, --dir <dir>', 'Project directory')
132
+ // .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
133
+ // .option('--lang <lang>', 'Language used (python, ruby, java)')
134
+ // .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
135
+ // .action(async (pattern, opts) => {
136
+ // if (!pattern.endsWith('.xml')) {
137
+ // pattern += '.xml';
138
+ // }
139
+ // let { javaTests, lang } = opts;
140
+ // if (javaTests === true) javaTests = 'src/test/java';
141
+ // lang = lang?.toLowerCase();
142
+ // const runReader = new XmlReader({ javaTests, lang });
143
+ // const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
144
+ // if (!files.length) {
145
+ // console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
146
+ // process.exit(1);
147
+ // }
148
+
123
149
  program
124
150
  .command('xml')
125
151
  .description('Parse XML reports and upload to Testomat.io')
@@ -273,6 +299,109 @@ program
273
299
  }
274
300
  });
275
301
 
302
+ program
303
+ .command('replay')
304
+ .description('Replay test data from debug file and re-send to Testomat.io')
305
+ .argument('[debug-file]', 'Path to debug file (defaults to /tmp/testomatio.debug.latest.json)')
306
+ .action(async (debugFile, opts) => {
307
+ // Use default debug file if none provided
308
+ if (!debugFile) {
309
+ debugFile = path.join(os.tmpdir(), 'testomatio.debug.latest.json');
310
+ }
311
+
312
+ if (!fs.existsSync(debugFile)) {
313
+ console.log(APP_PREFIX, `❌ Debug file not found: ${debugFile}`);
314
+ return process.exit(1);
315
+ }
316
+
317
+ console.log(APP_PREFIX, `🪲 Replaying data from debug file: ${debugFile}`);
318
+
319
+ try {
320
+ const fileContent = fs.readFileSync(debugFile, 'utf-8');
321
+ const lines = fileContent.trim().split('\n').filter(Boolean);
322
+
323
+ if (lines.length === 0) {
324
+ console.log(APP_PREFIX, '❌ Debug file is empty');
325
+ return process.exit(1);
326
+ }
327
+
328
+ let runParams = {};
329
+ let finishParams = {};
330
+ let parseErrors = 0;
331
+ const allTests = [];
332
+
333
+ // Parse debug file line by line
334
+ for (const [lineIndex, line] of lines.entries()) {
335
+ try {
336
+ const logEntry = JSON.parse(line);
337
+
338
+ if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
339
+ Object.assign(process.env, logEntry.testomatioEnvVars, process.env);
340
+ } else if (logEntry.action === 'createRun') {
341
+ runParams = logEntry.params || {};
342
+ } else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
343
+ allTests.push(...logEntry.tests);
344
+ } else if (logEntry.action === 'addTest' && logEntry.testId) {
345
+ allTests.push(logEntry.testId);
346
+ } else if (logEntry.actions === 'finishRun') {
347
+ finishParams = logEntry.params || {};
348
+ }
349
+ } catch (err) {
350
+ parseErrors++;
351
+ if (parseErrors <= 3) {
352
+ // Only show first 3 parse errors
353
+ console.warn(APP_PREFIX, `⚠️ Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
354
+ }
355
+ }
356
+ }
357
+
358
+ if (parseErrors > 3) {
359
+ console.warn(APP_PREFIX, `⚠️ ${parseErrors - 3} more parse errors occurred`);
360
+ }
361
+
362
+ console.log(APP_PREFIX, `📊 Found ${allTests.length} tests to replay`);
363
+
364
+ if (allTests.length === 0) {
365
+ console.log(APP_PREFIX, '❌ No test data found in debug file');
366
+ return process.exit(1);
367
+ }
368
+
369
+ const apiKey = config.TESTOMATIO;
370
+ if (!apiKey) {
371
+ console.log(APP_PREFIX, '❌ TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
372
+ return process.exit(1);
373
+ }
374
+
375
+ // Create client and restore the run
376
+ const client = new TestomatClient({
377
+ apiKey,
378
+ isBatchEnabled: true,
379
+ ...runParams,
380
+ });
381
+
382
+ console.log(APP_PREFIX, '🚀 Publishing to run...');
383
+ await client.createRun(runParams);
384
+
385
+ // Send each test result - let client.js handle the data mapping and formatting
386
+ for (const [index, test] of allTests.entries()) {
387
+ try {
388
+ await client.addTestRun(test.status, test);
389
+ } catch (err) {
390
+ console.warn(APP_PREFIX, `⚠️ Failed to send test ${index + 1}: ${err.message}`);
391
+ }
392
+ }
393
+
394
+ await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
395
+
396
+ console.log(APP_PREFIX, `✅ Successfully replayed ${allTests.length} tests from debug file`);
397
+ process.exit(0);
398
+ } catch (err) {
399
+ console.error(APP_PREFIX, '❌ Error replaying debug data:', err.message);
400
+ console.error(err.stack);
401
+ process.exit(1);
402
+ }
403
+ });
404
+
276
405
  program.parse(process.argv);
277
406
 
278
407
  if (!process.argv.slice(2).length) {
@@ -5,8 +5,11 @@ import { glob } from 'glob';
5
5
  import createDebugMessages from 'debug';
6
6
  import { APP_PREFIX } from '../constants.js';
7
7
  import XmlReader from '../xmlReader.js';
8
- import { version } from '../../package.json';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
9
  import dotenv from 'dotenv';
10
+ import path from 'path';
11
+
12
+ const version = getPackageVersion();
10
13
 
11
14
  const debug = createDebugMessages('@testomatio/reporter:xml-cli');
12
15
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
@@ -4,10 +4,11 @@ import { Command } from 'commander';
4
4
  import pc from 'picocolors';
5
5
  import TestomatClient from '../client.js';
6
6
  import { APP_PREFIX, STATUS } from '../constants.js';
7
- import { version } from '../../package.json';
7
+ import { getPackageVersion } from '../utils/utils.js';
8
8
  import { config } from '../config.js';
9
9
  import dotenv from 'dotenv';
10
10
 
11
+ const version = getPackageVersion();
11
12
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
12
13
  const program = new Command();
13
14
 
@@ -5,12 +5,13 @@ import pc from 'picocolors';
5
5
  import createDebugMessages from 'debug';
6
6
  import TestomatClient from '../client.js';
7
7
  import { APP_PREFIX } from '../constants.js';
8
- import { version } from '../../package.json';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
9
  import { config } from '../config.js';
10
10
  import { readLatestRunId } from '../utils/utils.js';
11
11
  import dotenv from 'dotenv';
12
12
 
13
13
  const debug = createDebugMessages('@testomatio/reporter:upload-cli');
14
+ const version = getPackageVersion();
14
15
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
15
16
  const program = new Command();
16
17
 
package/src/client.js CHANGED
@@ -31,7 +31,6 @@ class Client {
31
31
  * Create a Testomat client instance
32
32
  * @returns
33
33
  */
34
- // eslint-disable-next-line
35
34
  constructor(params = {}) {
36
35
  this.paramsForPipesFactory = params;
37
36
  this.pipeStore = {};
@@ -79,7 +78,7 @@ class Client {
79
78
  try {
80
79
  const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
81
80
 
82
- if (!filterPipe.isEnabled) {
81
+ if (!filterPipe?.isEnabled) {
83
82
  // TODO:for the future for the another pipes
84
83
  console.warn(
85
84
  APP_PREFIX,
package/src/config.js CHANGED
@@ -6,10 +6,10 @@ const debug = createDebugMessages('@testomatio/reporter:config');
6
6
  /* for possibility to use multiple env files (reading different paths)
7
7
  const envFileVars = dotenv.config({ path: '.env' }).parsed; */
8
8
 
9
- if (process.env.TESTOMATIO_API_KEY) {
9
+ if (process.env.TESTOMATIO_API_KEY && !process.env.TESTOMATIO) {
10
10
  process.env.TESTOMATIO = process.env.TESTOMATIO_API_KEY;
11
11
  }
12
- if (process.env.TESTOMATIO_TOKEN) {
12
+ if (process.env.TESTOMATIO_TOKEN && !process.env.TESTOMATIO) {
13
13
  process.env.TESTOMATIO = process.env.TESTOMATIO_TOKEN;
14
14
  }
15
15
 
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import Adapter from './adapter.js';
2
3
 
3
4
  class CSharpAdapter extends Adapter {
@@ -7,10 +8,21 @@ class CSharpAdapter extends Adapter {
7
8
  if (example) t.example = { ...example[1].split(',') };
8
9
  const suite = t.suite_title.split('.');
9
10
  t.suite_title = suite.pop();
10
- t.file = suite.join('/');
11
+ t.file = namespaceToFileName(t.file);
11
12
  t.title = title.trim();
12
13
  return t;
13
14
  }
15
+
16
+ getFilePath(t) {
17
+ const fileName = namespaceToFileName(t.file);
18
+ return fileName;
19
+ }
14
20
  }
15
21
 
16
22
  export default CSharpAdapter;
23
+
24
+ function namespaceToFileName(fileName) {
25
+ const fileParts = fileName.split('.');
26
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
27
+ return `${fileParts.join(path.sep)}.cs`;
28
+ }