@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
@@ -1,12 +1,6 @@
1
1
  import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
-
4
- // Retry interceptor function
5
- import axiosRetry from 'axios-retry';
6
-
7
- // Default axios instance
8
- import axios from 'axios';
9
-
3
+ import { Gaxios } from 'gaxios';
10
4
  import JsonCycle from 'json-cycle';
11
5
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
12
6
  import { isValidUrl, foundedTestLog } from '../utils/utils.js';
@@ -15,9 +9,7 @@ import { config } from '../config.js';
15
9
 
16
10
  const debug = createDebugMessages('@testomatio/reporter:pipe:testomatio');
17
11
 
18
- if (process.env.TESTOMATIO_RUN) {
19
- // process.env.runId = process.env.TESTOMATIO_RUN;
20
- }
12
+ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
21
13
 
22
14
  /**
23
15
  * @typedef {import('../../types/types.js').Pipe} Pipe
@@ -59,50 +51,38 @@ class TestomatioPipe {
59
51
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
60
52
  this.env = process.env.TESTOMATIO_ENV;
61
53
  this.label = process.env.TESTOMATIO_LABEL;
62
- // Create a new instance of axios with a custom config
63
- this.axios = axios.create({
54
+
55
+ // Create a new instance of gaxios with a custom config
56
+ this.client = new Gaxios({
64
57
  baseURL: `${this.url.trim()}`,
65
58
  timeout: AXIOS_TIMEOUT,
66
- proxy: proxy
67
- ? {
68
- host: proxy.hostname,
69
- port: parseInt(proxy.port, 10),
70
- protocol: proxy.protocol,
59
+ proxy: proxy ? proxy.toString() : undefined,
60
+ retry: true,
61
+ retryConfig: {
62
+ retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
63
+ retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
64
+ httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
65
+ shouldRetry: (error) => {
66
+ if (!error.response) return false;
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;
71
75
  }
72
- : false,
73
- });
74
-
75
- // Pass the axios instance to the retry function
76
- axiosRetry(this.axios, {
77
- // do not use retries for unit tests
78
- retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
79
- shouldResetTimeout: true,
80
- retryCondition: error => {
81
- if (!error.response) return false;
82
- switch (error.response?.status) {
83
- case 400: // Bad request (probably wrong API key)
84
- case 404: // Test not matched
85
- case 429: // Rate limit exceeded
86
- case 500: // Internal server error
87
- return false;
88
- default:
89
- break;
76
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
90
77
  }
91
- return error.response?.status >= 401; // Retry on 401+ and 5xx
92
- },
93
- retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
94
- onRetry: async (retryCount, error) => {
95
- this.retriesTimestamps.push(Date.now());
96
-
97
- debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
98
- },
78
+ }
99
79
  });
100
80
 
101
81
  this.isEnabled = true;
102
82
  // do not finish this run (for parallel testing)
103
83
  this.proceed = process.env.TESTOMATIO_PROCEED;
104
84
  this.jiraId = process.env.TESTOMATIO_JIRA_ID;
105
- this.runId = params.runId || process.env.runId;
85
+ this.runId = params.runId || process.env.TESTOMATIO_RUN;
106
86
  this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
107
87
  this.hasUnmatchedTests = false;
108
88
  this.requestFailures = 0;
@@ -136,12 +116,15 @@ class TestomatioPipe {
136
116
  return;
137
117
  }
138
118
 
139
- const resp = await this.axios.get('/api/test_grep', q);
140
- const { data } = resp;
119
+ const resp = await this.client.request({
120
+ method: 'GET',
121
+ url: '/api/test_grep',
122
+ params: q
123
+ });
141
124
 
142
- if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
143
- foundedTestLog(APP_PREFIX, data.tests);
144
- return data.tests;
125
+ if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
126
+ foundedTestLog(APP_PREFIX, resp.data.tests);
127
+ return resp.data.tests;
145
128
  }
146
129
 
147
130
  console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
@@ -201,16 +184,23 @@ class TestomatioPipe {
201
184
  if (this.runId) {
202
185
  this.store.runId = this.runId;
203
186
  debug(`Run with id ${this.runId} already created, updating...`);
204
- const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
187
+ const resp = await this.client.request({
188
+ method: 'PUT',
189
+ url: `/api/reporter/${this.runId}`,
190
+ data: runParams
191
+ });
205
192
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
206
193
  return;
207
194
  }
208
195
 
209
196
  debug('Creating run...');
210
197
  try {
211
- const resp = await this.axios.post(`/api/reporter`, runParams, {
198
+ const resp = await this.client.request({
199
+ method: 'POST',
200
+ url: '/api/reporter',
201
+ data: runParams,
212
202
  maxContentLength: Infinity,
213
- maxBodyLength: Infinity,
203
+ responseType: 'json'
214
204
  });
215
205
 
216
206
  this.runId = resp.data.uid;
@@ -227,6 +217,7 @@ class TestomatioPipe {
227
217
  debug('Run created', this.runId);
228
218
  } catch (err) {
229
219
  const errorText = err.response?.data?.message || err.message;
220
+ debug('Error creating run', err);
230
221
  console.log(errorText || err);
231
222
  if (!this.apiKey) console.error('Testomat.io API key is not set');
232
223
  if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
@@ -273,7 +264,15 @@ class TestomatioPipe {
273
264
 
274
265
  debug('Adding test', json);
275
266
 
276
- return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
267
+ return this.client.request({
268
+ method: 'POST',
269
+ url: `/api/reporter/${this.runId}/testrun`,
270
+ data: json,
271
+ headers: {
272
+ 'Content-Type': 'application/json',
273
+ },
274
+ maxContentLength: Infinity
275
+ }).catch(err => {
277
276
  this.requestFailures++;
278
277
  this.notReportedTestsCount++;
279
278
  if (err.response) {
@@ -325,44 +324,51 @@ class TestomatioPipe {
325
324
  const testsToSend = this.batch.tests.splice(0);
326
325
  debug('📨 Batch upload', testsToSend.length, 'tests');
327
326
 
328
- return this.axios
329
- .post(
330
- `/api/reporter/${this.runId}/testrun`,
331
- { api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex },
332
- axiosAddTestrunRequestConfig,
333
- )
334
- .catch(err => {
335
- this.requestFailures++;
336
- this.notReportedTestsCount += testsToSend.length;
337
- if (err.response) {
338
- if (err.response.status >= 400) {
339
- const responseData = err.response.data || { message: '' };
340
- console.log(
341
- APP_PREFIX,
342
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
343
- // pc.grey(data?.title || ''),
344
- );
345
- if (err.response?.data?.message?.includes('could not be matched')) {
346
- this.hasUnmatchedTests = true;
347
- }
348
- return;
349
- }
327
+ return this.client.request({
328
+ method: 'POST',
329
+ url: `/api/reporter/${this.runId}/testrun`,
330
+ data: {
331
+ api_key: this.apiKey,
332
+ tests: testsToSend,
333
+ batch_index: this.batch.batchIndex
334
+ },
335
+ headers: {
336
+ 'Content-Type': 'application/json',
337
+ },
338
+ maxContentLength: Infinity
339
+ }).catch(err => {
340
+ this.requestFailures++;
341
+ this.notReportedTestsCount += testsToSend.length;
342
+ if (err.response) {
343
+ if (err.response.status >= 400) {
344
+ const responseData = err.response.data || { message: '' };
350
345
  console.log(
351
346
  APP_PREFIX,
352
- pc.yellow(`Warning: (${err.response?.status})`),
353
- `Report couldn't be processed: ${err?.response?.data?.message}`,
347
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
354
348
  );
355
- printCreateIssue(err);
356
- } else {
357
- console.log(APP_PREFIX, "Report couldn't be processed", err);
349
+ if (err.response?.data?.message?.includes('could not be matched')) {
350
+ this.hasUnmatchedTests = true;
351
+ }
352
+ return;
358
353
  }
359
- });
354
+ console.log(
355
+ APP_PREFIX,
356
+ pc.yellow(`Warning: (${err.response?.status})`),
357
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
358
+ );
359
+ printCreateIssue(err);
360
+ } else {
361
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
362
+ }
363
+ });
360
364
  };
361
365
 
362
366
  /**
363
367
  * Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
364
368
  */
365
369
  addTest(data) {
370
+ this.isEnabled = this.apiKey ?? this.isEnabled;
371
+
366
372
  if (!this.isEnabled) return;
367
373
  if (!this.runId) return;
368
374
 
@@ -371,11 +377,15 @@ class TestomatioPipe {
371
377
  data.api_key = this.apiKey;
372
378
  data.create = this.createNewTests;
373
379
 
374
- if (!this.batch.isEnabled) this.#uploadSingleTest(data);
380
+ let uploading = null;
381
+ if (!this.batch.isEnabled) uploading = this.#uploadSingleTest(data);
375
382
  else this.batch.tests.push(data);
376
383
 
377
384
  // if test is added after run which is already finished
378
- if (!this.batch.intervalFunction) this.#batchUpload();
385
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
386
+
387
+ // return promise to be able to wait for it
388
+ return uploading;
379
389
  }
380
390
 
381
391
  /**
@@ -415,12 +425,16 @@ class TestomatioPipe {
415
425
 
416
426
  try {
417
427
  if (this.runId && !this.proceed) {
418
- await this.axios.put(`/api/reporter/${this.runId}`, {
419
- api_key: this.apiKey,
420
- duration: params.duration,
421
- status_event,
422
- detach: params.detach,
423
- tests: params.tests,
428
+ await this.client.request({
429
+ method: 'PUT',
430
+ url: `/api/reporter/${this.runId}`,
431
+ data: {
432
+ api_key: this.apiKey,
433
+ duration: params.duration,
434
+ status_event,
435
+ detach: params.detach,
436
+ tests: params.tests,
437
+ }
424
438
  });
425
439
  if (this.runUrl) {
426
440
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
@@ -437,14 +451,11 @@ class TestomatioPipe {
437
451
 
438
452
  if (this.hasUnmatchedTests) {
439
453
  console.log('');
440
- // eslint-disable-next-line max-len
441
454
  console.log(APP_PREFIX, pc.yellow(pc.bold('⚠️ Some reported tests were not found in Testomat.io project')));
442
- // eslint-disable-next-line max-len
443
455
  console.log(
444
456
  APP_PREFIX,
445
457
  `If you use Testomat.io as a reporter only, please re-run tests using ${pc.bold('TESTOMATIO_CREATE=1')}`,
446
458
  );
447
- // eslint-disable-next-line max-len
448
459
  console.log(
449
460
  APP_PREFIX,
450
461
  `But to keep your tests consistent it is recommended to ${pc.bold('import tests first')}`,
@@ -453,7 +464,6 @@ class TestomatioPipe {
453
464
  console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
454
465
  console.log(APP_PREFIX, pc.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
455
466
  console.log(APP_PREFIX, 'or for Cucumber:');
456
- // eslint-disable-next-line max-len
457
467
  console.log(APP_PREFIX, pc.bold('npx check-cucumber ... --update-ids'), 'See: https://bit.ly/bdd-update-ids');
458
468
  }
459
469
  } catch (err) {
@@ -478,26 +488,17 @@ function printCreateIssue(err) {
478
488
  console.log(
479
489
  APP_PREFIX,
480
490
  'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new',
481
- ); // eslint-disable-line max-len
491
+ );
482
492
  console.log(APP_PREFIX, 'Provide this information:');
483
493
  console.log('Error:', err.message || err.code);
484
494
  if (!err.config) return;
485
495
 
486
496
  const time = new Date().toUTCString();
487
- const { data, url, baseURL, method } = err?.config || {};
497
+ const { body, url, baseURL, method } = err?.config || {};
488
498
  console.log('```js');
489
- console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
499
+ console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
490
500
  console.log('```');
491
501
  });
492
502
  }
493
503
 
494
- const axiosAddTestrunRequestConfig = {
495
- maxContentLength: Infinity,
496
- maxBodyLength: Infinity,
497
- headers: {
498
- // Overwrite Axios's automatically set Content-Type
499
- 'Content-Type': 'application/json',
500
- },
501
- };
502
-
503
504
  export default TestomatioPipe;
package/src/replay.js ADDED
@@ -0,0 +1,262 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import TestomatClient from './client.js';
5
+ import { STATUS } from './constants.js';
6
+ import { config } from './config.js';
7
+
8
+ export class Replay {
9
+ constructor(options = {}) {
10
+ this.apiKey = options.apiKey || config.TESTOMATIO || undefined;
11
+ this.dryRun = options.dryRun || false;
12
+ this.onProgress = options.onProgress || (() => {});
13
+ this.onLog = options.onLog || console.log;
14
+ this.onError = options.onError || console.error;
15
+ }
16
+
17
+ /**
18
+ * Get the default debug file path
19
+ * @returns {string} Path to the latest debug file
20
+ */
21
+ getDefaultDebugFile() {
22
+ return path.join(os.tmpdir(), 'testomatio.debug.latest.json');
23
+ }
24
+
25
+ /**
26
+ * Parse a debug file and extract test data
27
+ * @param {string} debugFile - Path to the debug file
28
+ * @returns {Object} Parsed debug data
29
+ */
30
+ parseDebugFile(debugFile) {
31
+ if (!fs.existsSync(debugFile)) {
32
+ throw new Error(`Debug file not found: ${debugFile}`);
33
+ }
34
+
35
+ const fileContent = fs.readFileSync(debugFile, 'utf-8');
36
+ const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
37
+
38
+ if (lines.length === 0) {
39
+ throw new Error('Debug file is empty');
40
+ }
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
+
50
+ // Parse debug file line by line
51
+ for (const [lineIndex, line] of lines.entries()) {
52
+ try {
53
+ const logEntry = JSON.parse(line);
54
+
55
+ if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
56
+ Object.assign(envVars, logEntry.testomatioEnvVars);
57
+ } else if (logEntry.action === 'createRun') {
58
+ runParams = logEntry.params || {};
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
+ } else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
78
+ // Merge artifacts arrays
79
+ mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
80
+ } else if (existingTest[key] === null || existingTest[key] === undefined ||
81
+ (Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
82
+ // Use new value if existing is null/undefined/empty array
83
+ mergedTest[key] = test[key];
84
+ }
85
+ }
86
+ });
87
+ testsMap.set(test.rid, mergedTest);
88
+ } else {
89
+ testsMap.set(test.rid, { ...test });
90
+ }
91
+ } else {
92
+ // Handle tests without rid (no deduplication)
93
+ testsWithoutRid.push({ ...test });
94
+ }
95
+ }
96
+ } else if (logEntry.action === 'addTest' && logEntry.testId) {
97
+ // Extract runId if available
98
+ if (logEntry.runId && !runId) {
99
+ runId = logEntry.runId;
100
+ }
101
+ const test = logEntry.testId;
102
+ if (test.rid) {
103
+ // Handle tests with rid (deduplicate)
104
+ const existingTest = testsMap.get(test.rid);
105
+ if (existingTest) {
106
+ // Merge with existing test
107
+ const mergedTest = { ...existingTest, ...test };
108
+ testsMap.set(test.rid, mergedTest);
109
+ } else {
110
+ testsMap.set(test.rid, { ...test });
111
+ }
112
+ } else {
113
+ // Handle tests without rid (no deduplication)
114
+ testsWithoutRid.push({ ...test });
115
+ }
116
+ } else if (logEntry.actions === 'finishRun') {
117
+ finishParams = logEntry.params || {};
118
+ }
119
+ } catch (err) {
120
+ parseErrors++;
121
+ if (parseErrors <= 3) {
122
+ // Only show first 3 parse errors
123
+ this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
124
+ }
125
+ }
126
+ }
127
+
128
+ if (parseErrors > 3) {
129
+ this.onError(`${parseErrors - 3} more parse errors occurred`);
130
+ }
131
+
132
+ // Combine tests with rid and tests without rid
133
+ const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
134
+
135
+ return {
136
+ runParams,
137
+ finishParams,
138
+ tests: allTests,
139
+ envVars,
140
+ parseErrors,
141
+ totalLines: lines.length,
142
+ runId
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Restore environment variables from debug data
148
+ * @param {Object} envVars - Environment variables to restore
149
+ */
150
+ restoreEnvironmentVariables(envVars) {
151
+ // Only restore env vars that aren't already set (don't override current values)
152
+ Object.keys(envVars).forEach(key => {
153
+ if (process.env[key] === undefined || process.env[key] === '') {
154
+ process.env[key] = envVars[key];
155
+ }
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Replay test data to Testomat.io
161
+ * @param {string} debugFile - Path to debug file (optional, uses default if not provided)
162
+ * @returns {Promise<Object>} Replay results
163
+ */
164
+ async replay(debugFile) {
165
+ if (!debugFile) {
166
+ debugFile = this.getDefaultDebugFile();
167
+ }
168
+
169
+ if (!this.apiKey) {
170
+ throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
171
+ }
172
+
173
+ this.onLog(`Replaying data from debug file: ${debugFile}`);
174
+
175
+ // Parse the debug file
176
+ const debugData = this.parseDebugFile(debugFile);
177
+ const { runParams, finishParams, tests, envVars, runId } = debugData;
178
+
179
+ this.onLog(`Found ${tests.length} tests to replay`);
180
+
181
+ if (tests.length === 0) {
182
+ throw new Error('No test data found in debug file');
183
+ }
184
+
185
+ // Restore environment variables
186
+ this.restoreEnvironmentVariables(envVars);
187
+
188
+ if (this.dryRun) {
189
+ return {
190
+ success: true,
191
+ testsCount: tests.length,
192
+ runParams,
193
+ finishParams,
194
+ envVars,
195
+ runId,
196
+ dryRun: true
197
+ };
198
+ }
199
+
200
+ // Create client and restore the run
201
+ const client = new TestomatClient({
202
+ apiKey: this.apiKey,
203
+ isBatchEnabled: true,
204
+ ...runParams,
205
+ });
206
+
207
+ // Use the stored runId if available, otherwise create a new run
208
+ if (runId) {
209
+ this.onLog(`Using existing run ID: ${runId}`);
210
+ client.runId = runId;
211
+ } else {
212
+ this.onLog('Publishing to run...');
213
+ await client.createRun(runParams);
214
+ }
215
+
216
+ // Send each test result
217
+ let successCount = 0;
218
+ let failureCount = 0;
219
+
220
+ for (const [index, test] of tests.entries()) {
221
+ try {
222
+ await client.addTestRun(test.status, test);
223
+ successCount++;
224
+ this.onProgress({
225
+ current: index + 1,
226
+ total: tests.length,
227
+ test,
228
+ success: true
229
+ });
230
+ } catch (err) {
231
+ failureCount++;
232
+ this.onError(`Failed to send test ${index + 1}: ${err.message}`);
233
+ this.onProgress({
234
+ current: index + 1,
235
+ total: tests.length,
236
+ test,
237
+ success: false,
238
+ error: err.message
239
+ });
240
+ }
241
+ }
242
+
243
+ await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
244
+
245
+ const result = {
246
+ success: true,
247
+ testsCount: tests.length,
248
+ successCount,
249
+ failureCount,
250
+ runParams,
251
+ finishParams,
252
+ envVars,
253
+ runId: runId || client.runId
254
+ };
255
+
256
+ this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
257
+
258
+ return result;
259
+ }
260
+ }
261
+
262
+ export default Replay;
@@ -47,9 +47,50 @@ function setKeyValue(keyValue, value = null) {
47
47
  services.keyValues.put(keyValue);
48
48
  }
49
49
 
50
+ /**
51
+ * Add a single label to the test report
52
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
53
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
54
+ */
55
+ function setLabel(key, value = null) {
56
+ if (!key || typeof key !== 'string') {
57
+ console.warn('Label key must be a non-empty string');
58
+ return;
59
+ }
60
+
61
+ // Limit key length to 255 characters
62
+ if (key.length > 255) {
63
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
64
+ key = key.substring(0, 255);
65
+ }
66
+
67
+ let labelString = key;
68
+ if (value !== null && value !== undefined && value !== '') {
69
+ if (typeof value !== 'string') {
70
+ console.warn('Label value must be a string, converting:', value);
71
+ value = String(value);
72
+ }
73
+ // Limit value length to 255 characters
74
+ if (value.length > 255) {
75
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
76
+ value = value.substring(0, 255);
77
+ }
78
+ labelString = `${key}:${value}`;
79
+ }
80
+
81
+ // Limit total label length to 255 characters
82
+ if (labelString.length > 255) {
83
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
84
+ labelString = labelString.substring(0, 255);
85
+ }
86
+
87
+ services.labels.put([labelString]);
88
+ }
89
+
50
90
  export default {
51
91
  artifact: saveArtifact,
52
92
  log: logMessage,
53
93
  step: addStep,
54
94
  keyValue: setKeyValue,
95
+ label: setLabel,
55
96
  };
package/src/reporter.js CHANGED
@@ -8,6 +8,7 @@ export const log = reporterFunctions.log;
8
8
  export const logger = services.logger;
9
9
  export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
+ export const label = reporterFunctions.label;
11
12
 
12
13
  /**
13
14
  * @typedef {import('./reporter-functions.js')} artifact
@@ -15,6 +16,7 @@ export const step = reporterFunctions.step;
15
16
  * @typedef {import('./services/index.js')} logger
16
17
  * @typedef {import('./reporter-functions.js')} meta
17
18
  * @typedef {import('./reporter-functions.js')} step
19
+ * @typedef {import('./reporter-functions.js')} label
18
20
  */
19
21
  export default {
20
22
  /**
@@ -27,6 +29,7 @@ export default {
27
29
  logger: services.logger,
28
30
  meta: reporterFunctions.keyValue,
29
31
  step: reporterFunctions.step,
32
+ label: reporterFunctions.label,
30
33
 
31
34
  // TestomatClient,
32
35
  // TRConstants,
@@ -1,12 +1,14 @@
1
1
  import { logger } from './logger.js';
2
2
  import { artifactStorage } from './artifacts.js';
3
3
  import { keyValueStorage } from './key-values.js';
4
+ import { labelStorage } from './labels.js';
4
5
  import { dataStorage } from '../data-storage.js';
5
6
 
6
7
  export const services = {
7
8
  logger,
8
9
  artifacts: artifactStorage,
9
10
  keyValues: keyValueStorage,
11
+ labels: labelStorage,
10
12
  setContext: context => {
11
13
  dataStorage.setContext(context);
12
14
  },