@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +40 -11
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +57 -152
  7. package/lib/data-storage.d.ts +1 -1
  8. package/lib/helpers.d.ts +1 -0
  9. package/lib/helpers.js +4 -0
  10. package/lib/junit-adapter/csharp.d.ts +0 -1
  11. package/lib/junit-adapter/csharp.js +43 -7
  12. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  13. package/lib/junit-adapter/nunit-parser.js +433 -0
  14. package/lib/pipe/bitbucket.js +5 -5
  15. package/lib/pipe/coverage.d.ts +82 -0
  16. package/lib/pipe/coverage.js +373 -0
  17. package/lib/pipe/gitlab.js +4 -4
  18. package/lib/pipe/index.js +2 -0
  19. package/lib/pipe/testomatio.d.ts +3 -2
  20. package/lib/pipe/testomatio.js +44 -18
  21. package/lib/reporter-functions.js +14 -12
  22. package/lib/reporter.d.ts +31 -21
  23. package/lib/reporter.js +40 -5
  24. package/lib/services/artifacts.d.ts +1 -1
  25. package/lib/services/key-values.d.ts +1 -1
  26. package/lib/services/links.d.ts +1 -1
  27. package/lib/services/logger.d.ts +1 -1
  28. package/lib/uploader.js +4 -0
  29. package/lib/utils/log-formatter.d.ts +28 -0
  30. package/lib/utils/log-formatter.js +127 -0
  31. package/lib/utils/pipe_utils.d.ts +15 -0
  32. package/lib/utils/pipe_utils.js +44 -2
  33. package/lib/utils/utils.d.ts +6 -0
  34. package/lib/utils/utils.js +260 -25
  35. package/lib/xmlReader.d.ts +32 -26
  36. package/lib/xmlReader.js +121 -52
  37. package/package.json +12 -7
  38. package/src/adapter/codecept.js +19 -19
  39. package/src/adapter/mocha.js +1 -1
  40. package/src/adapter/playwright.js +2 -2
  41. package/src/bin/cli.js +51 -13
  42. package/src/bin/reportXml.js +5 -2
  43. package/src/client.js +69 -130
  44. package/src/helpers.js +1 -0
  45. package/src/junit-adapter/csharp.js +48 -6
  46. package/src/junit-adapter/nunit-parser.js +474 -0
  47. package/src/pipe/bitbucket.js +5 -5
  48. package/src/pipe/coverage.js +440 -0
  49. package/src/pipe/debug.js +1 -2
  50. package/src/pipe/gitlab.js +4 -4
  51. package/src/pipe/index.js +2 -0
  52. package/src/pipe/testomatio.js +109 -85
  53. package/src/reporter-functions.js +15 -12
  54. package/src/reporter.js +6 -4
  55. package/src/services/links.js +1 -1
  56. package/src/uploader.js +5 -0
  57. package/src/utils/log-formatter.js +113 -0
  58. package/src/utils/pipe_utils.js +52 -3
  59. package/src/utils/utils.js +277 -22
  60. package/src/xmlReader.js +144 -46
  61. package/types/types.d.ts +364 -0
  62. package/types/vitest.types.d.ts +93 -0
@@ -3,7 +3,12 @@ import pc from 'picocolors';
3
3
  import { Gaxios } from 'gaxios';
4
4
  import JsonCycle from 'json-cycle';
5
5
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
6
- import { isValidUrl, foundedTestLog, readLatestRunId, transformEnvVarToBoolean } from '../utils/utils.js';
6
+ import { isValidUrl,
7
+ foundedTestLog,
8
+ readLatestRunId,
9
+ transformEnvVarToBoolean,
10
+ getGitCommitSha
11
+ } from '../utils/utils.js';
7
12
  import { parseFilterParams, generateFilterRequestParams, setS3Credentials } from '../utils/pipe_utils.js';
8
13
  import { config } from '../config.js';
9
14
 
@@ -46,7 +51,25 @@ class TestomatioPipe {
46
51
  this.store = store || {};
47
52
  this.title = params.title || process.env.TESTOMATIO_TITLE;
48
53
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
49
- this.sharedRunTimeout = !!process.env.TESTOMATIO_SHARED_RUN_TIMEOUT;
54
+ this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
55
+ ? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
56
+ : undefined;
57
+
58
+ if (this.sharedRunTimeout && !this.sharedRun) {
59
+ debug('Auto-enabling sharedRun because sharedRunTimeout is set');
60
+ this.sharedRun = true;
61
+ }
62
+
63
+ if (!this.title && (this.sharedRun || this.sharedRunTimeout)) {
64
+ const sha = getGitCommitSha();
65
+ if (sha) {
66
+ this.title = `Shared Run - ${sha}`;
67
+ console.log(APP_PREFIX, `🔄 Auto-generated title for shared run: ${this.title}`);
68
+ } else {
69
+ console.log(APP_PREFIX, pc.red('Failed to resolve git commit SHA for shared run title.'));
70
+ console.log(APP_PREFIX, 'Please run the tests inside a Git repository or set TESTOMATIO_TITLE explicitly.');
71
+ }
72
+ }
50
73
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
51
74
  this.env = process.env.TESTOMATIO_ENV;
52
75
  this.label = process.env.TESTOMATIO_LABEL;
@@ -60,8 +83,8 @@ class TestomatioPipe {
60
83
  retryConfig: {
61
84
  retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
85
  retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
63
- httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
64
- shouldRetry: (error) => {
86
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
87
+ shouldRetry: error => {
65
88
  if (!error.response) return false;
66
89
  switch (error.response?.status) {
67
90
  case 400: // Bad request (probably wrong API key)
@@ -73,8 +96,8 @@ class TestomatioPipe {
73
96
  break;
74
97
  }
75
98
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- }
77
- }
99
+ },
100
+ },
78
101
  });
79
102
 
80
103
  this.isEnabled = true;
@@ -104,7 +127,6 @@ class TestomatioPipe {
104
127
  // add test ID + run ID
105
128
  if (data.rid) data.rid = `${this.runId}-${data.rid}`;
106
129
 
107
-
108
130
  if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
109
131
  data.stack = null;
110
132
  }
@@ -120,7 +142,6 @@ class TestomatioPipe {
120
142
  return data;
121
143
  }
122
144
 
123
-
124
145
  /**
125
146
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
126
147
  * @param {Object} opts - The options for preparing the test grepList.
@@ -131,17 +152,23 @@ class TestomatioPipe {
131
152
  async prepareRun(opts) {
132
153
  if (!this.isEnabled) return [];
133
154
 
134
- const { type, id } = parseFilterParams(opts);
155
+ const clearOptions = parseFilterParams(opts);
156
+
157
+ if (!clearOptions) {
158
+ return [];
159
+ }
160
+
161
+ const { type, id } = clearOptions;
135
162
 
136
163
  try {
137
164
  const q = generateFilterRequestParams({
138
165
  type,
139
166
  id,
140
- apiKey: this.apiKey.trim(),
167
+ apiKey: this?.apiKey?.trim(),
141
168
  });
142
169
 
143
170
  if (!q) {
144
- return;
171
+ return [];
145
172
  }
146
173
 
147
174
  const resp = await this.client.request({
@@ -163,7 +190,7 @@ class TestomatioPipe {
163
190
 
164
191
  /**
165
192
  * Creates a new run on Testomat.io
166
- * @param {{isBatchEnabled?: boolean}} params
193
+ * @param {{isBatchEnabled?: boolean, kind?: string}} params
167
194
  * @returns Promise<void>
168
195
  */
169
196
  async createRun(params = {}) {
@@ -204,6 +231,7 @@ class TestomatioPipe {
204
231
  label: this.label,
205
232
  shared_run: this.sharedRun,
206
233
  shared_run_timeout: this.sharedRunTimeout,
234
+ kind: params.kind,
207
235
  }).filter(([, value]) => !!value),
208
236
  );
209
237
  debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
@@ -215,7 +243,7 @@ class TestomatioPipe {
215
243
  method: 'PUT',
216
244
  url: `/api/reporter/${this.runId}`,
217
245
  data: runParams,
218
- responseType: 'json'
246
+ responseType: 'json',
219
247
  });
220
248
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
221
249
  return;
@@ -228,7 +256,7 @@ class TestomatioPipe {
228
256
  url: '/api/reporter',
229
257
  data: runParams,
230
258
  maxContentLength: Infinity,
231
- responseType: 'json'
259
+ responseType: 'json',
232
260
  });
233
261
 
234
262
  this.runId = resp.data.uid;
@@ -287,44 +315,44 @@ class TestomatioPipe {
287
315
 
288
316
  debug('Adding test', json);
289
317
 
290
- return this.client.request({
291
- method: 'POST',
292
- url: `/api/reporter/${this.runId}/testrun`,
293
- data: json,
294
- headers: {
295
- 'Content-Type': 'application/json',
296
- },
297
- maxContentLength: Infinity
298
- }).catch(err => {
299
- this.requestFailures++;
300
- this.notReportedTestsCount++;
301
- if (err.response) {
302
- if (err.response.status >= 400) {
303
- const responseData = err.response.data || { message: '' };
318
+ return this.client
319
+ .request({
320
+ method: 'POST',
321
+ url: `/api/reporter/${this.runId}/testrun`,
322
+ data: json,
323
+ headers: {
324
+ 'Content-Type': 'application/json',
325
+ },
326
+ maxContentLength: Infinity,
327
+ })
328
+ .catch(err => {
329
+ this.requestFailures++;
330
+ this.notReportedTestsCount++;
331
+ if (err.response) {
332
+ if (err.response.status >= 400) {
333
+ const responseData = err.response.data || { message: '' };
334
+ console.log(
335
+ APP_PREFIX,
336
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
337
+ pc.gray(data?.title || ''),
338
+ );
339
+ if (err.response?.data?.message?.includes('could not be matched')) {
340
+ this.hasUnmatchedTests = true;
341
+ }
342
+ return;
343
+ }
304
344
  console.log(
305
345
  APP_PREFIX,
306
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
- pc.gray(data?.title || ''),
346
+ pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
347
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
308
348
  );
309
- if (err.response?.data?.message?.includes('could not be matched')) {
310
- this.hasUnmatchedTests = true;
311
- }
312
- return;
349
+ printCreateIssue(err);
350
+ } else {
351
+ console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
313
352
  }
314
- console.log(
315
- APP_PREFIX,
316
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
- `Report couldn't be processed: ${err?.response?.data?.message}`,
318
- );
319
- printCreateIssue(err);
320
- } else {
321
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
322
- }
323
- });
353
+ });
324
354
  };
325
355
 
326
-
327
-
328
356
  /**
329
357
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
330
358
  */
@@ -349,43 +377,42 @@ class TestomatioPipe {
349
377
  const testsToSend = this.batch.tests.splice(0);
350
378
  debug('📨 Batch upload', testsToSend.length, 'tests');
351
379
 
352
- return this.client.request({
353
- method: 'POST',
354
- url: `/api/reporter/${this.runId}/testrun`,
355
- data: {
356
- api_key: this.apiKey,
357
- tests: testsToSend,
358
- batch_index: this.batch.batchIndex
359
- },
360
- headers: {
361
- 'Content-Type': 'application/json',
362
- },
363
- maxContentLength: Infinity
364
- }).catch(err => {
365
- this.requestFailures++;
366
- this.notReportedTestsCount += testsToSend.length;
367
- if (err.response) {
368
- if (err.response.status >= 400) {
369
- const responseData = err.response.data || { message: '' };
380
+ return this.client
381
+ .request({
382
+ method: 'POST',
383
+ url: `/api/reporter/${this.runId}/testrun`,
384
+ data: {
385
+ api_key: this.apiKey,
386
+ tests: testsToSend,
387
+ batch_index: this.batch.batchIndex,
388
+ },
389
+ headers: {
390
+ 'Content-Type': 'application/json',
391
+ },
392
+ maxContentLength: Infinity,
393
+ })
394
+ .catch(err => {
395
+ this.requestFailures++;
396
+ this.notReportedTestsCount += testsToSend.length;
397
+ if (err.response) {
398
+ if (err.response.status >= 400) {
399
+ const responseData = err.response.data || { message: '' };
400
+ console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
401
+ if (err.response?.data?.message?.includes('could not be matched')) {
402
+ this.hasUnmatchedTests = true;
403
+ }
404
+ return;
405
+ }
370
406
  console.log(
371
407
  APP_PREFIX,
372
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
408
+ pc.yellow(`Warning: (${err.response?.status})`),
409
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
373
410
  );
374
- if (err.response?.data?.message?.includes('could not be matched')) {
375
- this.hasUnmatchedTests = true;
376
- }
377
- return;
411
+ printCreateIssue(err);
412
+ } else {
413
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
378
414
  }
379
- console.log(
380
- APP_PREFIX,
381
- pc.yellow(`Warning: (${err.response?.status})`),
382
- `Report couldn't be processed: ${err?.response?.data?.message}`,
383
- );
384
- printCreateIssue(err);
385
- } else {
386
- console.log(APP_PREFIX, "Report couldn't be processed", err);
387
- }
388
- });
415
+ });
389
416
  };
390
417
 
391
418
  /**
@@ -408,9 +435,9 @@ class TestomatioPipe {
408
435
  else this.batch.tests.push(data);
409
436
 
410
437
  // if test is added after run which is already finished
411
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
438
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
412
439
 
413
- // return promise to be able to wait for it
440
+ // return promise to be able to wait for it
414
441
  return uploading;
415
442
  }
416
443
 
@@ -459,7 +486,7 @@ class TestomatioPipe {
459
486
  status_event,
460
487
  detach: params.detach,
461
488
  tests: params.tests,
462
- }
489
+ },
463
490
  });
464
491
 
465
492
  if (this.runUrl) {
@@ -525,9 +552,6 @@ function printCreateIssue(err) {
525
552
  console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
526
553
  console.log('```');
527
554
  });
528
-
529
555
  }
530
556
 
531
-
532
-
533
557
  export default TestomatioPipe;
@@ -1,3 +1,4 @@
1
+ import { isPlaywright } from './helpers.js';
1
2
  import { services } from './services/index.js';
2
3
 
3
4
  /**
@@ -7,9 +8,7 @@ import { services } from './services/index.js';
7
8
  * @returns {void}
8
9
  */
9
10
  function saveArtifact(data, context = null) {
10
- if (process.env.IS_PLAYWRIGHT)
11
- throw new Error(`This function is not available in Playwright framework.
12
- /Playwright supports artifacts out of the box`);
11
+ showPlaywrightWarning('artifact', 'Playwright supports artifacts out of the box.');
13
12
  if (!data) return;
14
13
  services.artifacts.put(data, context);
15
14
  }
@@ -20,7 +19,6 @@ function saveArtifact(data, context = null) {
20
19
  * @returns {void}
21
20
  */
22
21
  function logMessage(...args) {
23
- if (process.env.IS_PLAYWRIGHT) throw new Error('This function is not available in Playwright framework');
24
22
  services.logger._templateLiteralLog(...args);
25
23
  }
26
24
 
@@ -30,10 +28,10 @@ function logMessage(...args) {
30
28
  * @returns {void}
31
29
  */
32
30
  function addStep(message) {
33
- if (process.env.IS_PLAYWRIGHT)
34
- throw new Error('This function is not available in Playwright framework. Use playwright steps');
35
-
36
31
  services.logger.step(message);
32
+ // this is done because Playwright reporter intercepts console logs and then we gather them and show on Testomat
33
+ // if not console.log, the step message will be lost from reporter
34
+ if (isPlaywright) console.log(`Step: ${message}`);
37
35
  }
38
36
 
39
37
  /**
@@ -43,8 +41,7 @@ function addStep(message) {
43
41
  * @returns {void}
44
42
  */
45
43
  function setKeyValue(keyValue, value = null) {
46
- if (process.env.IS_PLAYWRIGHT)
47
- throw new Error('This function is not available in Playwright framework. Use test tag instead.');
44
+ showPlaywrightWarning('meta', 'Use test annotations instead.');
48
45
 
49
46
  if (typeof keyValue === 'string') {
50
47
  keyValue = { [keyValue]: value };
@@ -59,12 +56,12 @@ function setKeyValue(keyValue, value = null) {
59
56
  * @returns {void}
60
57
  */
61
58
  function setLabel(key, value = null) {
59
+ showPlaywrightWarning('label', 'Use test tag instead.');
62
60
  if (Array.isArray(value)) {
63
61
  return value.forEach(label => setLabel(key, label));
64
62
  }
65
- const labelObject = value !== null && value !== undefined && value !== ''
66
- ? { label: `${key}:${value}` }
67
- : { label: key };
63
+ const labelObject =
64
+ value !== null && value !== undefined && value !== '' ? { label: `${key}:${value}` } : { label: key };
68
65
  services.links.put([labelObject]);
69
66
  }
70
67
 
@@ -88,6 +85,12 @@ function linkJira(...jiraIds) {
88
85
  services.links.put(links);
89
86
  }
90
87
 
88
+ function showPlaywrightWarning(functionName, recommendation) {
89
+ if (isPlaywright) {
90
+ console.warn(`[TESTOMATIO] '${functionName}' function is not supported for Playwright. ${recommendation}`);
91
+ }
92
+ }
93
+
91
94
  export default {
92
95
  artifact: saveArtifact,
93
96
  log: logMessage,
package/src/reporter.js CHANGED
@@ -1,8 +1,10 @@
1
- // import TestomatClient from './client.js';
2
- // import * as TRConstants from './constants.js';
1
+ import Client from './client.js';
2
+ import * as TestomatioConstants from './constants.js';
3
3
  import { services } from './services/index.js';
4
4
  import reporterFunctions from './reporter-functions.js';
5
5
 
6
+ export { Client };
7
+ export const STATUS = TestomatioConstants.STATUS;
6
8
  export const artifact = reporterFunctions.artifact;
7
9
  export const log = reporterFunctions.log;
8
10
  export const logger = services.logger;
@@ -35,6 +37,6 @@ export default {
35
37
  linkTest: reporterFunctions.linkTest,
36
38
  linkJira: reporterFunctions.linkJira,
37
39
 
38
- // TestomatClient,
39
- // TRConstants,
40
+ TestomatioClient: Client,
41
+ STATUS,
40
42
  };
@@ -66,4 +66,4 @@ class LinkStorage {
66
66
  }
67
67
  }
68
68
 
69
- export const linkStorage = LinkStorage.getInstance();
69
+ export const linkStorage = LinkStorage.getInstance();
package/src/uploader.js CHANGED
@@ -194,6 +194,11 @@ export class S3Uploader {
194
194
  filePath = path.join(process.cwd(), filePath);
195
195
  }
196
196
 
197
+ // Normalize path separators for cross-platform compatibility
198
+ if (typeof filePath === 'string') {
199
+ filePath = filePath.replace(/\\/g, '/');
200
+ }
201
+
197
202
  const data = { rid, file: filePath, uploaded };
198
203
  const jsonLine = `${JSON.stringify(data)}\n`;
199
204
  fs.appendFileSync(tempFilePath, jsonLine);
@@ -0,0 +1,113 @@
1
+ import createCallsiteRecord from 'callsite-record';
2
+ import { minimatch } from 'minimatch';
3
+ import pc from 'picocolors';
4
+ import { stripVTControlCharacters } from 'util';
5
+ import { sep } from 'path';
6
+ import { formatStep, truncate } from './utils.js';
7
+
8
+ const stripColors = stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
9
+
10
+ /**
11
+ * Returns the formatted stack including the stack trace, steps, and logs.
12
+ * @param {Object} params - Parameters for formatting logs
13
+ * @param {string} params.error - Error message
14
+ * @param {Array|any} params.steps - Test steps (array or other types)
15
+ * @param {string} params.logs - Test logs
16
+ * @returns {string}
17
+ */
18
+ export function formatLogs({ error, steps, logs }) {
19
+ error = error?.trim();
20
+ logs = logs
21
+ ?.trim()
22
+ .split('\n')
23
+ .map(l => truncate(l))
24
+ .join('\n');
25
+
26
+ if (Array.isArray(steps)) {
27
+ steps = steps
28
+ .map(step => formatStep(step))
29
+ .flat()
30
+ .join('\n');
31
+ } else {
32
+ steps = null;
33
+ }
34
+
35
+ let testLogs = '';
36
+ if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
37
+ if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
38
+ if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
39
+ return testLogs;
40
+ }
41
+
42
+ /**
43
+ * Formats an error with stack trace and diff information
44
+ * @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
45
+ * The error object to format
46
+ * @param {string} [message] - Optional error message override
47
+ * @returns {string}
48
+ */
49
+ export function formatError(error, message) {
50
+ if (!message) message = error.message;
51
+ // @ts-ignore - inspect is a custom property added by some testing frameworks
52
+ if (error.inspect) message = error.inspect() || '';
53
+
54
+ let stack = '';
55
+ if (error.name) stack += `${pc.red(error.name)}`;
56
+ // @ts-ignore - operator is a custom property added by assertion libraries
57
+ if (error.operator) stack += ` (${pc.red(error.operator)})`;
58
+ // add new line if something was added to stack
59
+ if (stack) stack += ': ';
60
+
61
+ stack += `${message}\n`;
62
+
63
+ // @ts-ignore - diff is a custom property added by vitest
64
+ if (error.diff) {
65
+ // diff for vitest
66
+ stack += error.diff;
67
+ stack += '\n\n';
68
+ } else if (error.actual && error.expected && error.actual !== error.expected) {
69
+ // diffs for mocha, cypress, codeceptjs style
70
+ stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
71
+ stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
72
+ stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
73
+ stack += '\n\n';
74
+ }
75
+
76
+ const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
77
+
78
+ try {
79
+ let hasFrame = false;
80
+ const record = createCallsiteRecord({
81
+ forError: error,
82
+ isCallsiteFrame: frame => {
83
+ if (customFilter && minimatch(frame.fileName, customFilter)) return false;
84
+ if (hasFrame) return false;
85
+ if (isNotInternalFrame(frame)) hasFrame = true;
86
+ return hasFrame;
87
+ },
88
+ });
89
+ // @ts-ignore
90
+ if (record && !record.filename.startsWith('http')) {
91
+ stack += record.renderSync({ stackFilter: isNotInternalFrame });
92
+ }
93
+ return stack;
94
+ } catch (e) {
95
+ console.log(e);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Checks if a stack frame is not an internal frame (node_modules or internal)
101
+ * @param {Object} frame - Stack frame object
102
+ * @returns {boolean}
103
+ */
104
+ function isNotInternalFrame(frame) {
105
+ return (
106
+ frame.getFileName() &&
107
+ frame.getFileName().includes(sep) &&
108
+ !frame.getFileName().includes('node_modules') &&
109
+ !frame.getFileName().includes('internal')
110
+ );
111
+ }
112
+
113
+ export { stripColors };
@@ -15,6 +15,7 @@ function setS3Credentials(artifacts) {
15
15
  if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
16
16
  if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
17
17
  if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
18
+ if (artifacts.stack_artifacts) process.env.TESTOMATIO_STACK_ARTIFACTS = '1';
18
19
  // endpoint is not received from the server; and shuld be empty if IAM used (credentails obtained from the testomat)
19
20
  process.env.S3_ENDPOINT = artifacts.ENDPOINT || '';
20
21
  }
@@ -25,6 +26,12 @@ function setS3Credentials(artifacts) {
25
26
  * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
26
27
  */
27
28
  function generateFilterRequestParams(params) {
29
+ // Defensive check: ensure params is an object
30
+ if (!params || typeof params !== 'object') {
31
+ console.error(APP_PREFIX, `Invalid parameters provided. Expected an object, got: ${typeof params}`);
32
+ return;
33
+ }
34
+
28
35
  const { type, id, apiKey } = params;
29
36
 
30
37
  if (!type) {
@@ -53,9 +60,13 @@ function generateFilterRequestParams(params) {
53
60
  * The object has properties "type" and "id".
54
61
  */
55
62
  function parseFilterParams(opts) {
56
- const [type, id] = opts.split('=');
63
+ const [type, ...idParts] = opts.split('=');
64
+ const id = idParts.join('=');
65
+
57
66
  const validType = updateFilterType(type);
58
67
 
68
+ if (!validType) return undefined;
69
+
59
70
  return {
60
71
  type: validType,
61
72
  id,
@@ -69,6 +80,8 @@ function parseFilterParams(opts) {
69
80
  * Returns undefined if the type is not valid.
70
81
  */
71
82
  function updateFilterType(type) {
83
+ if (!type || typeof type !== 'string') return;
84
+
72
85
  let typeLowerCase = type.toLowerCase();
73
86
 
74
87
  const filterTypes = ['tag-name', 'plan', 'label', 'jira-ticket'];
@@ -86,7 +99,7 @@ function updateFilterType(type) {
86
99
  ];
87
100
 
88
101
  if (!filterTypes.includes(typeLowerCase)) {
89
- console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
102
+ console.log(APP_PREFIX, `❗❗❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
90
103
  return;
91
104
  }
92
105
 
@@ -120,4 +133,40 @@ function fullName(t) {
120
133
  return line;
121
134
  }
122
135
 
123
- export { updateFilterType, parseFilterParams, generateFilterRequestParams, setS3Credentials, statusEmoji, fullName };
136
+ /**
137
+ * Parses a comma-separated list of key-value pairs into an options object.
138
+ *
139
+ * The input string should be formatted as `"key1=value1,key2=value2,..."`.
140
+ * Whitespace around keys and values is trimmed. If the input is empty or undefined,
141
+ * an empty object is returned.
142
+ *
143
+ * @param {string} [optionsStr] - A comma-separated string of key=value pairs.
144
+ * @returns {Object} An object mapping option keys to their string values.
145
+ *
146
+ * @example
147
+ * parsePipeOptions('foo=bar,baz=qux');
148
+ * => Returns: { foo: 'bar', baz: 'qux' }
149
+ */
150
+ function parsePipeOptions(optionsStr) {
151
+ const options = {};
152
+ if (!optionsStr) return options;
153
+
154
+ const pairs = optionsStr.split(',');
155
+ for (const pair of pairs) {
156
+ const [key, value] = pair.split('=');
157
+ if (key && value) {
158
+ options[key.trim()] = value.trim();
159
+ }
160
+ }
161
+ return options;
162
+ }
163
+
164
+ export {
165
+ updateFilterType,
166
+ parseFilterParams,
167
+ generateFilterRequestParams,
168
+ setS3Credentials,
169
+ statusEmoji,
170
+ fullName,
171
+ parsePipeOptions
172
+ };