@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.1-codeceptjs

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 (80) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.js +288 -202
  3. package/lib/adapter/cypress-plugin/index.js +0 -2
  4. package/lib/adapter/mocha.js +0 -1
  5. package/lib/adapter/nightwatch.js +5 -5
  6. package/lib/adapter/playwright.js +11 -3
  7. package/lib/adapter/webdriver.d.ts +1 -1
  8. package/lib/adapter/webdriver.js +18 -8
  9. package/lib/bin/cli.js +73 -8
  10. package/lib/bin/reportXml.js +4 -2
  11. package/lib/bin/startTest.js +3 -2
  12. package/lib/bin/uploadArtifacts.js +5 -4
  13. package/lib/client.js +30 -10
  14. package/lib/data-storage.d.ts +5 -5
  15. package/lib/data-storage.js +23 -13
  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 +27 -6
  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.js +0 -3
  26. package/lib/pipe/index.js +17 -7
  27. package/lib/pipe/testomatio.d.ts +3 -2
  28. package/lib/pipe/testomatio.js +85 -75
  29. package/lib/replay.d.ts +31 -0
  30. package/lib/replay.js +255 -0
  31. package/lib/reporter-functions.d.ts +7 -0
  32. package/lib/reporter-functions.js +36 -0
  33. package/lib/reporter.d.ts +15 -12
  34. package/lib/reporter.js +4 -1
  35. package/lib/services/artifacts.d.ts +1 -1
  36. package/lib/services/index.d.ts +2 -0
  37. package/lib/services/index.js +2 -0
  38. package/lib/services/key-values.d.ts +1 -1
  39. package/lib/services/labels.d.ts +22 -0
  40. package/lib/services/labels.js +62 -0
  41. package/lib/services/logger.d.ts +1 -1
  42. package/lib/services/logger.js +1 -2
  43. package/lib/template/testomatio.hbs +443 -68
  44. package/lib/uploader.js +10 -6
  45. package/lib/utils/constants.d.ts +12 -0
  46. package/lib/utils/constants.js +15 -0
  47. package/lib/utils/utils.d.ts +10 -1
  48. package/lib/utils/utils.js +70 -22
  49. package/lib/xmlReader.js +54 -19
  50. package/package.json +16 -11
  51. package/src/adapter/codecept.js +320 -214
  52. package/src/adapter/cypress-plugin/index.js +0 -2
  53. package/src/adapter/mocha.js +0 -1
  54. package/src/adapter/nightwatch.js +1 -1
  55. package/src/adapter/playwright.js +10 -7
  56. package/src/adapter/webdriver.js +2 -2
  57. package/src/bin/cli.js +70 -2
  58. package/src/bin/reportXml.js +4 -1
  59. package/src/bin/startTest.js +2 -1
  60. package/src/bin/uploadArtifacts.js +2 -1
  61. package/src/client.js +18 -3
  62. package/src/data-storage.js +6 -6
  63. package/src/junit-adapter/csharp.js +13 -1
  64. package/src/pipe/bitbucket.js +22 -24
  65. package/src/pipe/debug.js +26 -5
  66. package/src/pipe/github.js +1 -2
  67. package/src/pipe/gitlab.js +27 -9
  68. package/src/pipe/html.js +1 -4
  69. package/src/pipe/testomatio.js +106 -105
  70. package/src/replay.js +262 -0
  71. package/src/reporter-functions.js +41 -0
  72. package/src/reporter.js +3 -0
  73. package/src/services/index.js +2 -0
  74. package/src/services/labels.js +59 -0
  75. package/src/services/logger.js +1 -2
  76. package/src/template/testomatio.hbs +443 -68
  77. package/src/uploader.js +11 -6
  78. package/src/utils/constants.js +12 -0
  79. package/src/utils/utils.js +46 -13
  80. package/src/xmlReader.js +70 -18
@@ -5,19 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const debug_1 = __importDefault(require("debug"));
7
7
  const picocolors_1 = __importDefault(require("picocolors"));
8
- // Retry interceptor function
9
- const axios_retry_1 = __importDefault(require("axios-retry"));
10
- // Default axios instance
11
- const axios_1 = __importDefault(require("axios"));
8
+ const gaxios_1 = require("gaxios");
12
9
  const json_cycle_1 = __importDefault(require("json-cycle"));
13
10
  const constants_js_1 = require("../constants.js");
14
11
  const utils_js_1 = require("../utils/utils.js");
15
12
  const pipe_utils_js_1 = require("../utils/pipe_utils.js");
16
13
  const config_js_1 = require("../config.js");
17
14
  const debug = (0, debug_1.default)('@testomatio/reporter:pipe:testomatio');
18
- if (process.env.TESTOMATIO_RUN) {
19
- // process.env.runId = process.env.TESTOMATIO_RUN;
20
- }
15
+ if (process.env.TESTOMATIO_RUN)
16
+ process.env.runId = process.env.TESTOMATIO_RUN;
21
17
  /**
22
18
  * @typedef {import('../../types/types.js').Pipe} Pipe
23
19
  * @typedef {import('../../types/types.js').TestData} TestData
@@ -55,48 +51,37 @@ class TestomatioPipe {
55
51
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
56
52
  this.env = process.env.TESTOMATIO_ENV;
57
53
  this.label = process.env.TESTOMATIO_LABEL;
58
- // Create a new instance of axios with a custom config
59
- this.axios = axios_1.default.create({
54
+ // Create a new instance of gaxios with a custom config
55
+ this.client = new gaxios_1.Gaxios({
60
56
  baseURL: `${this.url.trim()}`,
61
57
  timeout: constants_js_1.AXIOS_TIMEOUT,
62
- proxy: proxy
63
- ? {
64
- host: proxy.hostname,
65
- port: parseInt(proxy.port, 10),
66
- protocol: proxy.protocol,
67
- }
68
- : false,
69
- });
70
- // Pass the axios instance to the retry function
71
- (0, axios_retry_1.default)(this.axios, {
72
- // do not use retries for unit tests
73
- retries: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
74
- shouldResetTimeout: true,
75
- retryCondition: error => {
76
- if (!error.response)
77
- return false;
78
- switch (error.response?.status) {
79
- case 400: // Bad request (probably wrong API key)
80
- case 404: // Test not matched
81
- case 429: // Rate limit exceeded
82
- case 500: // Internal server error
58
+ proxy: proxy ? proxy.toString() : undefined,
59
+ retry: true,
60
+ retryConfig: {
61
+ retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
+ retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
63
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
+ shouldRetry: (error) => {
65
+ if (!error.response)
83
66
  return false;
84
- default:
85
- break;
67
+ switch (error.response?.status) {
68
+ case 400: // Bad request (probably wrong API key)
69
+ case 404: // Test not matched
70
+ case 429: // Rate limit exceeded
71
+ case 500: // Internal server error
72
+ return false;
73
+ default:
74
+ break;
75
+ }
76
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
86
77
  }
87
- return error.response?.status >= 401; // Retry on 401+ and 5xx
88
- },
89
- retryDelay: () => constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
90
- onRetry: async (retryCount, error) => {
91
- this.retriesTimestamps.push(Date.now());
92
- debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
93
- },
78
+ }
94
79
  });
95
80
  this.isEnabled = true;
96
81
  // do not finish this run (for parallel testing)
97
82
  this.proceed = process.env.TESTOMATIO_PROCEED;
98
83
  this.jiraId = process.env.TESTOMATIO_JIRA_ID;
99
- this.runId = params.runId || process.env.runId;
84
+ this.runId = params.runId || process.env.TESTOMATIO_RUN;
100
85
  this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
101
86
  this.hasUnmatchedTests = false;
102
87
  this.requestFailures = 0;
@@ -125,11 +110,14 @@ class TestomatioPipe {
125
110
  if (!q) {
126
111
  return;
127
112
  }
128
- const resp = await this.axios.get('/api/test_grep', q);
129
- const { data } = resp;
130
- if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
131
- (0, utils_js_1.foundedTestLog)(constants_js_1.APP_PREFIX, data.tests);
132
- return data.tests;
113
+ const resp = await this.client.request({
114
+ method: 'GET',
115
+ url: '/api/test_grep',
116
+ params: q
117
+ });
118
+ if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
119
+ (0, utils_js_1.foundedTestLog)(constants_js_1.APP_PREFIX, resp.data.tests);
120
+ return resp.data.tests;
133
121
  }
134
122
  console.log(constants_js_1.APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
135
123
  }
@@ -181,16 +169,23 @@ class TestomatioPipe {
181
169
  if (this.runId) {
182
170
  this.store.runId = this.runId;
183
171
  debug(`Run with id ${this.runId} already created, updating...`);
184
- const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
172
+ const resp = await this.client.request({
173
+ method: 'PUT',
174
+ url: `/api/reporter/${this.runId}`,
175
+ data: runParams
176
+ });
185
177
  if (resp.data.artifacts)
186
178
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
187
179
  return;
188
180
  }
189
181
  debug('Creating run...');
190
182
  try {
191
- const resp = await this.axios.post(`/api/reporter`, runParams, {
183
+ const resp = await this.client.request({
184
+ method: 'POST',
185
+ url: '/api/reporter',
186
+ data: runParams,
192
187
  maxContentLength: Infinity,
193
- maxBodyLength: Infinity,
188
+ responseType: 'json'
194
189
  });
195
190
  this.runId = resp.data.uid;
196
191
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -206,6 +201,7 @@ class TestomatioPipe {
206
201
  }
207
202
  catch (err) {
208
203
  const errorText = err.response?.data?.message || err.message;
204
+ debug('Error creating run', err);
209
205
  console.log(errorText || err);
210
206
  if (!this.apiKey)
211
207
  console.error('Testomat.io API key is not set');
@@ -246,7 +242,15 @@ class TestomatioPipe {
246
242
  }
247
243
  const json = json_cycle_1.default.stringify(data);
248
244
  debug('Adding test', json);
249
- return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
245
+ return this.client.request({
246
+ method: 'POST',
247
+ url: `/api/reporter/${this.runId}/testrun`,
248
+ data: json,
249
+ headers: {
250
+ 'Content-Type': 'application/json',
251
+ },
252
+ maxContentLength: Infinity
253
+ }).catch(err => {
250
254
  this.requestFailures++;
251
255
  this.notReportedTestsCount++;
252
256
  if (err.response) {
@@ -291,9 +295,19 @@ class TestomatioPipe {
291
295
  // get tests from batch and clear batch
292
296
  const testsToSend = this.batch.tests.splice(0);
293
297
  debug('📨 Batch upload', testsToSend.length, 'tests');
294
- return this.axios
295
- .post(`/api/reporter/${this.runId}/testrun`, { api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex }, axiosAddTestrunRequestConfig)
296
- .catch(err => {
298
+ return this.client.request({
299
+ method: 'POST',
300
+ url: `/api/reporter/${this.runId}/testrun`,
301
+ data: {
302
+ api_key: this.apiKey,
303
+ tests: testsToSend,
304
+ batch_index: this.batch.batchIndex
305
+ },
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ },
309
+ maxContentLength: Infinity
310
+ }).catch(err => {
297
311
  this.requestFailures++;
298
312
  this.notReportedTestsCount += testsToSend.length;
299
313
  if (err.response) {
@@ -317,6 +331,7 @@ class TestomatioPipe {
317
331
  * Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
318
332
  */
319
333
  addTest(data) {
334
+ this.isEnabled = this.apiKey ?? this.isEnabled;
320
335
  if (!this.isEnabled)
321
336
  return;
322
337
  if (!this.runId)
@@ -326,13 +341,16 @@ class TestomatioPipe {
326
341
  data.rid = `${this.runId}-${data.rid}`;
327
342
  data.api_key = this.apiKey;
328
343
  data.create = this.createNewTests;
344
+ let uploading = null;
329
345
  if (!this.batch.isEnabled)
330
- this.#uploadSingleTest(data);
346
+ uploading = this.#uploadSingleTest(data);
331
347
  else
332
348
  this.batch.tests.push(data);
333
349
  // if test is added after run which is already finished
334
350
  if (!this.batch.intervalFunction)
335
- this.#batchUpload();
351
+ uploading = this.#batchUpload();
352
+ // return promise to be able to wait for it
353
+ return uploading;
336
354
  }
337
355
  /**
338
356
  * @param {import('../../types/types.js').RunData} params
@@ -367,12 +385,16 @@ class TestomatioPipe {
367
385
  status_event += '_parallel';
368
386
  try {
369
387
  if (this.runId && !this.proceed) {
370
- await this.axios.put(`/api/reporter/${this.runId}`, {
371
- api_key: this.apiKey,
372
- duration: params.duration,
373
- status_event,
374
- detach: params.detach,
375
- tests: params.tests,
388
+ await this.client.request({
389
+ method: 'PUT',
390
+ url: `/api/reporter/${this.runId}`,
391
+ data: {
392
+ api_key: this.apiKey,
393
+ duration: params.duration,
394
+ status_event,
395
+ detach: params.detach,
396
+ tests: params.tests,
397
+ }
376
398
  });
377
399
  if (this.runUrl) {
378
400
  console.log(constants_js_1.APP_PREFIX, '📊 Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
@@ -388,17 +410,13 @@ class TestomatioPipe {
388
410
  }
389
411
  if (this.hasUnmatchedTests) {
390
412
  console.log('');
391
- // eslint-disable-next-line max-len
392
413
  console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(picocolors_1.default.bold('⚠️ Some reported tests were not found in Testomat.io project')));
393
- // eslint-disable-next-line max-len
394
414
  console.log(constants_js_1.APP_PREFIX, `If you use Testomat.io as a reporter only, please re-run tests using ${picocolors_1.default.bold('TESTOMATIO_CREATE=1')}`);
395
- // eslint-disable-next-line max-len
396
415
  console.log(constants_js_1.APP_PREFIX, `But to keep your tests consistent it is recommended to ${picocolors_1.default.bold('import tests first')}`);
397
416
  console.log(constants_js_1.APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
398
417
  console.log(constants_js_1.APP_PREFIX, 'You can do that automatically via command line tools:');
399
418
  console.log(constants_js_1.APP_PREFIX, picocolors_1.default.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
400
419
  console.log(constants_js_1.APP_PREFIX, 'or for Cucumber:');
401
- // eslint-disable-next-line max-len
402
420
  console.log(constants_js_1.APP_PREFIX, picocolors_1.default.bold('npx check-cucumber ... --update-ids'), 'See: https://bit.ly/bdd-update-ids');
403
421
  }
404
422
  }
@@ -420,24 +438,16 @@ function printCreateIssue(err) {
420
438
  process.on('exit', () => {
421
439
  console.log();
422
440
  console.log(constants_js_1.APP_PREFIX, 'There was an error reporting to Testomat.io:');
423
- console.log(constants_js_1.APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new'); // eslint-disable-line max-len
441
+ console.log(constants_js_1.APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new');
424
442
  console.log(constants_js_1.APP_PREFIX, 'Provide this information:');
425
443
  console.log('Error:', err.message || err.code);
426
444
  if (!err.config)
427
445
  return;
428
446
  const time = new Date().toUTCString();
429
- const { data, url, baseURL, method } = err?.config || {};
447
+ const { body, url, baseURL, method } = err?.config || {};
430
448
  console.log('```js');
431
- console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
449
+ console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
432
450
  console.log('```');
433
451
  });
434
452
  }
435
- const axiosAddTestrunRequestConfig = {
436
- maxContentLength: Infinity,
437
- maxBodyLength: Infinity,
438
- headers: {
439
- // Overwrite Axios's automatically set Content-Type
440
- 'Content-Type': 'application/json',
441
- },
442
- };
443
453
  module.exports = TestomatioPipe;
@@ -0,0 +1,31 @@
1
+ export class Replay {
2
+ constructor(options?: {});
3
+ apiKey: any;
4
+ dryRun: any;
5
+ onProgress: any;
6
+ onLog: any;
7
+ onError: any;
8
+ /**
9
+ * Get the default debug file path
10
+ * @returns {string} Path to the latest debug file
11
+ */
12
+ getDefaultDebugFile(): string;
13
+ /**
14
+ * Parse a debug file and extract test data
15
+ * @param {string} debugFile - Path to the debug file
16
+ * @returns {Object} Parsed debug data
17
+ */
18
+ parseDebugFile(debugFile: string): any;
19
+ /**
20
+ * Restore environment variables from debug data
21
+ * @param {Object} envVars - Environment variables to restore
22
+ */
23
+ restoreEnvironmentVariables(envVars: any): void;
24
+ /**
25
+ * Replay test data to Testomat.io
26
+ * @param {string} debugFile - Path to debug file (optional, uses default if not provided)
27
+ * @returns {Promise<Object>} Replay results
28
+ */
29
+ replay(debugFile: string): Promise<any>;
30
+ }
31
+ export default Replay;
package/lib/replay.js ADDED
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Replay = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const client_js_1 = __importDefault(require("./client.js"));
11
+ const constants_js_1 = require("./constants.js");
12
+ const config_js_1 = require("./config.js");
13
+ class Replay {
14
+ constructor(options = {}) {
15
+ this.apiKey = options.apiKey || config_js_1.config.TESTOMATIO || undefined;
16
+ this.dryRun = options.dryRun || false;
17
+ this.onProgress = options.onProgress || (() => { });
18
+ this.onLog = options.onLog || console.log;
19
+ this.onError = options.onError || console.error;
20
+ }
21
+ /**
22
+ * Get the default debug file path
23
+ * @returns {string} Path to the latest debug file
24
+ */
25
+ getDefaultDebugFile() {
26
+ return path_1.default.join(os_1.default.tmpdir(), 'testomatio.debug.latest.json');
27
+ }
28
+ /**
29
+ * Parse a debug file and extract test data
30
+ * @param {string} debugFile - Path to the debug file
31
+ * @returns {Object} Parsed debug data
32
+ */
33
+ parseDebugFile(debugFile) {
34
+ if (!fs_1.default.existsSync(debugFile)) {
35
+ throw new Error(`Debug file not found: ${debugFile}`);
36
+ }
37
+ const fileContent = fs_1.default.readFileSync(debugFile, 'utf-8');
38
+ const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
39
+ if (lines.length === 0) {
40
+ throw new Error('Debug file is empty');
41
+ }
42
+ let runParams = {};
43
+ let finishParams = {};
44
+ let parseErrors = 0;
45
+ const testsMap = new Map(); // Use Map to deduplicate by rid
46
+ const testsWithoutRid = []; // For tests without rid (backward compatibility)
47
+ const envVars = {};
48
+ let runId = null;
49
+ // Parse debug file line by line
50
+ for (const [lineIndex, line] of lines.entries()) {
51
+ try {
52
+ const logEntry = JSON.parse(line);
53
+ if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
54
+ Object.assign(envVars, logEntry.testomatioEnvVars);
55
+ }
56
+ else if (logEntry.action === 'createRun') {
57
+ runParams = logEntry.params || {};
58
+ }
59
+ else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
60
+ // Extract runId if available
61
+ if (logEntry.runId && !runId) {
62
+ runId = logEntry.runId;
63
+ }
64
+ // Process each test in the batch
65
+ for (const test of logEntry.tests) {
66
+ if (test.rid) {
67
+ // Handle tests with rid (deduplicate)
68
+ const existingTest = testsMap.get(test.rid);
69
+ if (existingTest) {
70
+ // Merge test data - prioritize non-null/non-empty values
71
+ const mergedTest = { ...existingTest };
72
+ Object.keys(test).forEach(key => {
73
+ if (test[key] !== null && test[key] !== undefined) {
74
+ if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
75
+ // Merge files arrays
76
+ mergedTest.files = [...(existingTest.files || []), ...test[key]];
77
+ }
78
+ else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
79
+ // Merge artifacts arrays
80
+ mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
81
+ }
82
+ else if (existingTest[key] === null || existingTest[key] === undefined ||
83
+ (Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
84
+ // Use new value if existing is null/undefined/empty array
85
+ mergedTest[key] = test[key];
86
+ }
87
+ }
88
+ });
89
+ testsMap.set(test.rid, mergedTest);
90
+ }
91
+ else {
92
+ testsMap.set(test.rid, { ...test });
93
+ }
94
+ }
95
+ else {
96
+ // Handle tests without rid (no deduplication)
97
+ testsWithoutRid.push({ ...test });
98
+ }
99
+ }
100
+ }
101
+ else if (logEntry.action === 'addTest' && logEntry.testId) {
102
+ // Extract runId if available
103
+ if (logEntry.runId && !runId) {
104
+ runId = logEntry.runId;
105
+ }
106
+ const test = logEntry.testId;
107
+ if (test.rid) {
108
+ // Handle tests with rid (deduplicate)
109
+ const existingTest = testsMap.get(test.rid);
110
+ if (existingTest) {
111
+ // Merge with existing test
112
+ const mergedTest = { ...existingTest, ...test };
113
+ testsMap.set(test.rid, mergedTest);
114
+ }
115
+ else {
116
+ testsMap.set(test.rid, { ...test });
117
+ }
118
+ }
119
+ else {
120
+ // Handle tests without rid (no deduplication)
121
+ testsWithoutRid.push({ ...test });
122
+ }
123
+ }
124
+ else if (logEntry.actions === 'finishRun') {
125
+ finishParams = logEntry.params || {};
126
+ }
127
+ }
128
+ catch (err) {
129
+ parseErrors++;
130
+ if (parseErrors <= 3) {
131
+ // Only show first 3 parse errors
132
+ this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
133
+ }
134
+ }
135
+ }
136
+ if (parseErrors > 3) {
137
+ this.onError(`${parseErrors - 3} more parse errors occurred`);
138
+ }
139
+ // Combine tests with rid and tests without rid
140
+ const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
141
+ return {
142
+ runParams,
143
+ finishParams,
144
+ tests: allTests,
145
+ envVars,
146
+ parseErrors,
147
+ totalLines: lines.length,
148
+ runId
149
+ };
150
+ }
151
+ /**
152
+ * Restore environment variables from debug data
153
+ * @param {Object} envVars - Environment variables to restore
154
+ */
155
+ restoreEnvironmentVariables(envVars) {
156
+ // Only restore env vars that aren't already set (don't override current values)
157
+ Object.keys(envVars).forEach(key => {
158
+ if (process.env[key] === undefined || process.env[key] === '') {
159
+ process.env[key] = envVars[key];
160
+ }
161
+ });
162
+ }
163
+ /**
164
+ * Replay test data to Testomat.io
165
+ * @param {string} debugFile - Path to debug file (optional, uses default if not provided)
166
+ * @returns {Promise<Object>} Replay results
167
+ */
168
+ async replay(debugFile) {
169
+ if (!debugFile) {
170
+ debugFile = this.getDefaultDebugFile();
171
+ }
172
+ if (!this.apiKey) {
173
+ throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
174
+ }
175
+ this.onLog(`Replaying data from debug file: ${debugFile}`);
176
+ // Parse the debug file
177
+ const debugData = this.parseDebugFile(debugFile);
178
+ const { runParams, finishParams, tests, envVars, runId } = debugData;
179
+ this.onLog(`Found ${tests.length} tests to replay`);
180
+ if (tests.length === 0) {
181
+ throw new Error('No test data found in debug file');
182
+ }
183
+ // Restore environment variables
184
+ this.restoreEnvironmentVariables(envVars);
185
+ if (this.dryRun) {
186
+ return {
187
+ success: true,
188
+ testsCount: tests.length,
189
+ runParams,
190
+ finishParams,
191
+ envVars,
192
+ runId,
193
+ dryRun: true
194
+ };
195
+ }
196
+ // Create client and restore the run
197
+ const client = new client_js_1.default({
198
+ apiKey: this.apiKey,
199
+ isBatchEnabled: true,
200
+ ...runParams,
201
+ });
202
+ // Use the stored runId if available, otherwise create a new run
203
+ if (runId) {
204
+ this.onLog(`Using existing run ID: ${runId}`);
205
+ client.runId = runId;
206
+ }
207
+ else {
208
+ this.onLog('Publishing to run...');
209
+ await client.createRun(runParams);
210
+ }
211
+ // Send each test result
212
+ let successCount = 0;
213
+ let failureCount = 0;
214
+ for (const [index, test] of tests.entries()) {
215
+ try {
216
+ await client.addTestRun(test.status, test);
217
+ successCount++;
218
+ this.onProgress({
219
+ current: index + 1,
220
+ total: tests.length,
221
+ test,
222
+ success: true
223
+ });
224
+ }
225
+ catch (err) {
226
+ failureCount++;
227
+ this.onError(`Failed to send test ${index + 1}: ${err.message}`);
228
+ this.onProgress({
229
+ current: index + 1,
230
+ total: tests.length,
231
+ test,
232
+ success: false,
233
+ error: err.message
234
+ });
235
+ }
236
+ }
237
+ await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED, finishParams.parallel || false);
238
+ const result = {
239
+ success: true,
240
+ testsCount: tests.length,
241
+ successCount,
242
+ failureCount,
243
+ runParams,
244
+ finishParams,
245
+ envVars,
246
+ runId: runId || client.runId
247
+ };
248
+ this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
249
+ return result;
250
+ }
251
+ }
252
+ exports.Replay = Replay;
253
+ module.exports = Replay;
254
+
255
+ module.exports.Replay = Replay;
@@ -3,6 +3,7 @@ declare namespace _default {
3
3
  export { logMessage as log };
4
4
  export { addStep as step };
5
5
  export { setKeyValue as keyValue };
6
+ export { setLabel as label };
6
7
  }
7
8
  export default _default;
8
9
  /**
@@ -32,3 +33,9 @@ declare function addStep(message: string): void;
32
33
  declare function setKeyValue(keyValue: {
33
34
  [key: string]: string;
34
35
  } | string, value?: string | null): void;
36
+ /**
37
+ * Add a single label to the test report
38
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
39
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
40
+ */
41
+ declare function setLabel(key: string, value?: string): void;
@@ -44,9 +44,45 @@ function setKeyValue(keyValue, value = null) {
44
44
  }
45
45
  index_js_1.services.keyValues.put(keyValue);
46
46
  }
47
+ /**
48
+ * Add a single label to the test report
49
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
50
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
51
+ */
52
+ function setLabel(key, value = null) {
53
+ if (!key || typeof key !== 'string') {
54
+ console.warn('Label key must be a non-empty string');
55
+ return;
56
+ }
57
+ // Limit key length to 255 characters
58
+ if (key.length > 255) {
59
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
60
+ key = key.substring(0, 255);
61
+ }
62
+ let labelString = key;
63
+ if (value !== null && value !== undefined && value !== '') {
64
+ if (typeof value !== 'string') {
65
+ console.warn('Label value must be a string, converting:', value);
66
+ value = String(value);
67
+ }
68
+ // Limit value length to 255 characters
69
+ if (value.length > 255) {
70
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
71
+ value = value.substring(0, 255);
72
+ }
73
+ labelString = `${key}:${value}`;
74
+ }
75
+ // Limit total label length to 255 characters
76
+ if (labelString.length > 255) {
77
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
78
+ labelString = labelString.substring(0, 255);
79
+ }
80
+ index_js_1.services.labels.put([labelString]);
81
+ }
47
82
  module.exports = {
48
83
  artifact: saveArtifact,
49
84
  log: logMessage,
50
85
  step: addStep,
51
86
  keyValue: setKeyValue,
87
+ label: setLabel,
52
88
  };