@testomatio/reporter 2.7.0 → 2.7.2-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 (51) hide show
  1. package/README.md +2 -1
  2. package/lib/adapter/codecept.js +81 -26
  3. package/lib/adapter/playwright.d.ts +1 -1
  4. package/lib/adapter/playwright.js +54 -34
  5. package/lib/adapter/utils/step-formatter.d.ts +134 -0
  6. package/lib/adapter/utils/step-formatter.js +237 -0
  7. package/lib/bin/cli.js +28 -31
  8. package/lib/bin/reportXml.js +5 -6
  9. package/lib/bin/startTest.js +2 -1
  10. package/lib/bin/uploadArtifacts.js +6 -6
  11. package/lib/client.d.ts +8 -0
  12. package/lib/client.js +71 -10
  13. package/lib/constants.d.ts +1 -0
  14. package/lib/constants.js +7 -1
  15. package/lib/pipe/bitbucket.js +2 -1
  16. package/lib/pipe/coverage.js +16 -15
  17. package/lib/pipe/debug.js +3 -3
  18. package/lib/pipe/github.js +3 -2
  19. package/lib/pipe/gitlab.js +2 -1
  20. package/lib/pipe/index.js +5 -5
  21. package/lib/pipe/testomatio.js +21 -24
  22. package/lib/uploader.js +3 -2
  23. package/lib/utils/log.d.ts +45 -0
  24. package/lib/utils/log.js +98 -0
  25. package/lib/utils/pipe_utils.js +5 -5
  26. package/lib/utils/utils.d.ts +10 -0
  27. package/lib/utils/utils.js +16 -1
  28. package/lib/xmlReader.js +5 -4
  29. package/package.json +1 -1
  30. package/src/adapter/codecept.js +99 -29
  31. package/src/adapter/playwright.js +64 -39
  32. package/src/adapter/utils/step-formatter.js +232 -0
  33. package/src/bin/cli.js +34 -31
  34. package/src/bin/reportXml.js +5 -6
  35. package/src/bin/startTest.js +3 -2
  36. package/src/bin/uploadArtifacts.js +6 -6
  37. package/src/client.js +76 -26
  38. package/src/constants.js +4 -0
  39. package/src/pipe/bitbucket.js +2 -1
  40. package/src/pipe/coverage.js +16 -15
  41. package/src/pipe/debug.js +3 -3
  42. package/src/pipe/github.js +4 -3
  43. package/src/pipe/gitlab.js +2 -1
  44. package/src/pipe/index.js +5 -7
  45. package/src/pipe/testomatio.js +32 -25
  46. package/src/uploader.js +3 -2
  47. package/src/utils/log.js +87 -0
  48. package/src/utils/pipe_utils.js +5 -5
  49. package/src/utils/utils.js +14 -0
  50. package/src/xmlReader.js +5 -4
  51. package/types/types.d.ts +3 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Get the current log level from TESTOMATIO_LOG_LEVEL environment variable.
3
+ * Defaults to INFO (info, warn, and error messages).
4
+ * @returns {number} Numeric log level (0-2)
5
+ */
6
+ export function getLogLevel(): number;
7
+ /**
8
+ * Check if a message should be logged based on its level.
9
+ * A message is logged if its level is <= the current log level,
10
+ * or if TESTOMATIO_DEBUG is set (for debugging with the debug package).
11
+ * @param {number} messageLevel - Message level (LOG_LEVELS value)
12
+ * @returns {boolean} True if the message should be logged
13
+ */
14
+ export function shouldLog(messageLevel: number): boolean;
15
+ /**
16
+ * Log an info message with [TESTOMATIO] prefix.
17
+ * Only logs when TESTOMATIO_LOG_LEVEL is INFO.
18
+ * @param {...any} args - Arguments to log
19
+ */
20
+ export function info(...args: any[]): void;
21
+ /**
22
+ * Log a warning message with [TESTOMATIO] prefix.
23
+ * Only logs when TESTOMATIO_LOG_LEVEL is WARN or INFO.
24
+ * @param {...any} args - Arguments to log
25
+ */
26
+ export function warn(...args: any[]): void;
27
+ /**
28
+ * Log an error message with [TESTOMATIO] prefix.
29
+ * Logs for all TESTOMATIO_LOG_LEVEL values.
30
+ * @param {...any} args - Arguments to log
31
+ */
32
+ export function error(...args: any[]): void;
33
+ export namespace LOG_LEVELS {
34
+ let ERROR: number;
35
+ let WARN: number;
36
+ let INFO: number;
37
+ }
38
+ export namespace log {
39
+ export { info };
40
+ export { warn };
41
+ export { error };
42
+ export { getLogLevel };
43
+ export { shouldLog };
44
+ export { LOG_LEVELS };
45
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.log = exports.LOG_LEVELS = void 0;
4
+ exports.getLogLevel = getLogLevel;
5
+ exports.shouldLog = shouldLog;
6
+ exports.info = info;
7
+ exports.warn = warn;
8
+ exports.error = error;
9
+ const constants_js_1 = require("../constants.js");
10
+ /**
11
+ * Log levels for the Testomat.io reporter.
12
+ * A message is logged if its level is <= the current log level.
13
+ * @example
14
+ * TESTOMATIO_LOG_LEVEL=ERROR npx codeceptjs run // Only errors
15
+ * TESTOMATIO_LOG_LEVEL=WARN npx codeceptjs run // Warnings and errors
16
+ * TESTOMATIO_LOG_LEVEL=INFO npx codeceptjs run // Info, warnings, errors (default)
17
+ */
18
+ exports.LOG_LEVELS = {
19
+ ERROR: 0,
20
+ WARN: 1,
21
+ INFO: 2,
22
+ };
23
+ /**
24
+ * Get the current log level from TESTOMATIO_LOG_LEVEL environment variable.
25
+ * Defaults to INFO (info, warn, and error messages).
26
+ * @returns {number} Numeric log level (0-2)
27
+ */
28
+ function getLogLevel() {
29
+ const envLevel = process.env.TESTOMATIO_LOG_LEVEL?.toUpperCase();
30
+ return exports.LOG_LEVELS[envLevel] ?? exports.LOG_LEVELS.INFO;
31
+ }
32
+ /**
33
+ * Check if a message should be logged based on its level.
34
+ * A message is logged if its level is <= the current log level,
35
+ * or if TESTOMATIO_DEBUG is set (for debugging with the debug package).
36
+ * @param {number} messageLevel - Message level (LOG_LEVELS value)
37
+ * @returns {boolean} True if the message should be logged
38
+ */
39
+ function shouldLog(messageLevel) {
40
+ return messageLevel <= getLogLevel() || !!process.env.TESTOMATIO_DEBUG;
41
+ }
42
+ /**
43
+ * Log an info message with [TESTOMATIO] prefix.
44
+ * Only logs when TESTOMATIO_LOG_LEVEL is INFO.
45
+ * @param {...any} args - Arguments to log
46
+ */
47
+ function info(...args) {
48
+ if (shouldLog(exports.LOG_LEVELS.INFO)) {
49
+ console.log(constants_js_1.APP_PREFIX, ...args);
50
+ }
51
+ }
52
+ /**
53
+ * Log a warning message with [TESTOMATIO] prefix.
54
+ * Only logs when TESTOMATIO_LOG_LEVEL is WARN or INFO.
55
+ * @param {...any} args - Arguments to log
56
+ */
57
+ function warn(...args) {
58
+ if (shouldLog(exports.LOG_LEVELS.WARN)) {
59
+ console.warn(constants_js_1.APP_PREFIX, ...args);
60
+ }
61
+ }
62
+ /**
63
+ * Log an error message with [TESTOMATIO] prefix.
64
+ * Logs for all TESTOMATIO_LOG_LEVEL values.
65
+ * @param {...any} args - Arguments to log
66
+ */
67
+ function error(...args) {
68
+ if (shouldLog(exports.LOG_LEVELS.ERROR)) {
69
+ console.error(constants_js_1.APP_PREFIX, ...args);
70
+ }
71
+ }
72
+ /**
73
+ * Logging utility for Testomat.io reporter.
74
+ * All messages are prefixed with [TESTOMATIO] and respect TESTOMATIO_LOG_LEVEL.
75
+ * @example
76
+ * import { log } from './utils/log.js';
77
+ * log.info('Test started');
78
+ * log.warn('This is a warning');
79
+ * log.error('Something went wrong');
80
+ */
81
+ exports.log = {
82
+ info,
83
+ warn,
84
+ error,
85
+ getLogLevel,
86
+ shouldLog,
87
+ LOG_LEVELS: exports.LOG_LEVELS,
88
+ };
89
+
90
+ module.exports.getLogLevel = getLogLevel;
91
+
92
+ module.exports.shouldLog = shouldLog;
93
+
94
+ module.exports.info = info;
95
+
96
+ module.exports.warn = warn;
97
+
98
+ module.exports.error = error;
@@ -7,7 +7,7 @@ exports.setS3Credentials = setS3Credentials;
7
7
  exports.statusEmoji = statusEmoji;
8
8
  exports.fullName = fullName;
9
9
  exports.parsePipeOptions = parsePipeOptions;
10
- const constants_js_1 = require("../constants.js");
10
+ const log_js_1 = require("./log.js");
11
11
  /**
12
12
  * Set S3 credentials from the provided artifacts object.
13
13
  * @param {Object} artifacts - The artifacts object containing S3 credentials.
@@ -15,7 +15,7 @@ const constants_js_1 = require("../constants.js");
15
15
  function setS3Credentials(artifacts) {
16
16
  if (!Object.keys(artifacts).length)
17
17
  return;
18
- console.log(constants_js_1.APP_PREFIX, 'S3 credentials obtained from Testomat.io...');
18
+ log_js_1.log.info('S3 credentials obtained from Testomat.io...');
19
19
  if (artifacts.ACCESS_KEY_ID)
20
20
  process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
21
21
  if (artifacts.SECRET_ACCESS_KEY)
@@ -41,7 +41,7 @@ function setS3Credentials(artifacts) {
41
41
  function generateFilterRequestParams(params) {
42
42
  // Defensive check: ensure params is an object
43
43
  if (!params || typeof params !== 'object') {
44
- console.error(constants_js_1.APP_PREFIX, `Invalid parameters provided. Expected an object, got: ${typeof params}`);
44
+ log_js_1.log.error(`Invalid parameters provided. Expected an object, got: ${typeof params}`);
45
45
  return;
46
46
  }
47
47
  const { type, id, apiKey } = params;
@@ -49,7 +49,7 @@ function generateFilterRequestParams(params) {
49
49
  return;
50
50
  }
51
51
  if (!id) {
52
- console.error(constants_js_1.APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
52
+ log_js_1.log.error(`Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
53
53
  return;
54
54
  }
55
55
  return {
@@ -100,7 +100,7 @@ function updateFilterType(type) {
100
100
  // "ims-issue", //TODO: WIP
101
101
  ];
102
102
  if (!filterTypes.includes(typeLowerCase)) {
103
- console.log(constants_js_1.APP_PREFIX, `❗❗❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
103
+ log_js_1.log.error(`❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
104
104
  return;
105
105
  }
106
106
  const index = filterTypes.indexOf(typeLowerCase);
@@ -29,6 +29,16 @@ export function getGitCommitSha(): string | null;
29
29
  */
30
30
  export function getTestomatIdFromTestTitle(testTitle: string): string | null;
31
31
  export function humanize(text: any): any;
32
+ /**
33
+ * Checks whether a value is an HTTP(S) URL.
34
+ *
35
+ * Used for artifact handling: if a step artifact is already a remote URL,
36
+ * it should not be uploaded again as a local file path.
37
+ *
38
+ * @param {*} value - Artifact value to validate
39
+ * @returns {boolean} true when value starts with http:// or https://
40
+ */
41
+ export function isHttpUrl(value: any): boolean;
32
42
  export function isValidUrl(s: any): boolean;
33
43
  /**
34
44
  * @param {String} suiteTitle - suite title
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getGitCommitSha = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
39
+ exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.isHttpUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getGitCommitSha = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
40
40
  exports.getPackageVersion = getPackageVersion;
41
41
  exports.truncate = truncate;
42
42
  exports.cleanLatestRunId = cleanLatestRunId;
@@ -134,6 +134,19 @@ const isValidUrl = s => {
134
134
  }
135
135
  };
136
136
  exports.isValidUrl = isValidUrl;
137
+ /**
138
+ * Checks whether a value is an HTTP(S) URL.
139
+ *
140
+ * Used for artifact handling: if a step artifact is already a remote URL,
141
+ * it should not be uploaded again as a local file path.
142
+ *
143
+ * @param {*} value - Artifact value to validate
144
+ * @returns {boolean} true when value starts with http:// or https://
145
+ */
146
+ const isHttpUrl = value => {
147
+ return /^https?:\/\//i.test(String(value || ''));
148
+ };
149
+ exports.isHttpUrl = isHttpUrl;
137
150
  const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
138
151
  const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
139
152
  let files = Array.from(stack.matchAll(fileMatchRegex))
@@ -738,6 +751,8 @@ module.exports.ansiRegExp = ansiRegExp;
738
751
 
739
752
  module.exports.isValidUrl = isValidUrl;
740
753
 
754
+ module.exports.isHttpUrl = isHttpUrl;
755
+
741
756
  module.exports.fetchFilesFromStackTrace = fetchFilesFromStackTrace;
742
757
 
743
758
  module.exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
package/lib/xmlReader.js CHANGED
@@ -17,6 +17,7 @@ const index_js_1 = require("./pipe/index.js");
17
17
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
18
18
  const config_js_1 = require("./config.js");
19
19
  const uploader_js_1 = require("./uploader.js");
20
+ const log_js_1 = require("./utils/log.js");
20
21
  // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
21
22
  const debug = (0, debug_1.default)('@testomatio/reporter:xml');
22
23
  const ridRunId = (0, crypto_1.randomUUID)();
@@ -62,7 +63,7 @@ class XmlReader {
62
63
  // @ts-ignore
63
64
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
64
65
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
65
- console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
66
+ log_js_1.log.info(`Testomatio Reporter v${this.version}`);
66
67
  }
67
68
  connectAdapter() {
68
69
  if (this.opts.javaTests) {
@@ -435,7 +436,7 @@ class XmlReader {
435
436
  continue;
436
437
  const runId = this.runId || this.store.runId || Date.now().toString();
437
438
  test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path_1.default.basename(f)])));
438
- console.log(constants_js_1.APP_PREFIX, `🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
439
+ log_js_1.log.info(`🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
439
440
  }
440
441
  }
441
442
  async createRun() {
@@ -528,10 +529,10 @@ class XmlReader {
528
529
  debug(`Uploaded ${uploadedTests}/${totalTests} tests`);
529
530
  }
530
531
  if (totalChunks > 1) {
531
- console.log(constants_js_1.APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests in ${totalChunks} chunks`);
532
+ log_js_1.log.info(`✅ Successfully uploaded ${uploadedTests} tests in ${totalChunks} chunks`);
532
533
  }
533
534
  else {
534
- console.log(constants_js_1.APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests`);
535
+ log_js_1.log.info(`✅ Successfully uploaded ${uploadedTests} tests`);
535
536
  }
536
537
  const finishData = {
537
538
  api_key: this.requestParams.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.7.0",
3
+ "version": "2.7.2-beta.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -1,11 +1,13 @@
1
1
  import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
3
  import TestomatClient from '../client.js';
4
- import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
4
+ import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR, SCREENSHOTS_ON_STEPS } from '../constants.js';
5
5
  import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
6
6
  import { services } from '../services/index.js';
7
7
  import { dataStorage } from '../data-storage.js';
8
+ import { formatStep, addStatusToStep, addArtifactsToStep, addArtifactPathToStep } from './utils/step-formatter.js';
8
9
  import codeceptjs from 'codeceptjs';
10
+ import { log } from '../utils/log.js';
9
11
 
10
12
  const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
11
13
  // @ts-ignore
@@ -48,6 +50,7 @@ function CodeceptReporter(config) {
48
50
  let videos = [];
49
51
  let traces = [];
50
52
  const reportTestPromises = [];
53
+ let isRunFinalized = false;
51
54
 
52
55
  const testTimeMap = {};
53
56
  const { apiKey } = config;
@@ -84,6 +87,19 @@ function CodeceptReporter(config) {
84
87
  const hookSteps = new Map();
85
88
  let currentHook = null;
86
89
 
90
+ const finalizeRun = async origin => {
91
+ if (isRunFinalized) return;
92
+ isRunFinalized = true;
93
+
94
+ debug(`finalizing run from ${origin}`);
95
+ debug('waiting for all tests to be reported');
96
+
97
+ await Promise.allSettled(reportTestPromises);
98
+ await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
99
+ await uploadAttachments(client, traces, '📁 Uploading', 'trace');
100
+ await client.updateRunStatus('finished');
101
+ };
102
+
87
103
  event.dispatcher.on(event.workers.before, () => {
88
104
  recorder.add('Creating new run', async () => {
89
105
  await client.createRun();
@@ -94,7 +110,9 @@ function CodeceptReporter(config) {
94
110
  });
95
111
 
96
112
  event.dispatcher.on(event.workers.after, () => {
97
- client.updateRunStatus('finished');
113
+ recorder.add('Finishing run', async () => {
114
+ await finalizeRun('workers.after');
115
+ });
98
116
  });
99
117
 
100
118
  // Listening to events
@@ -108,6 +126,8 @@ function CodeceptReporter(config) {
108
126
  });
109
127
  videos = [];
110
128
  traces = [];
129
+ isRunFinalized = false;
130
+ reportTestPromises.length = 0;
111
131
 
112
132
  if (!global.testomatioDataStore) global.testomatioDataStore = {};
113
133
  });
@@ -141,7 +161,7 @@ function CodeceptReporter(config) {
141
161
  const error = hook?.ctx?.currentTest?.err;
142
162
 
143
163
  for (const test of suite.tests) {
144
- client.addTestRun('failed', {
164
+ const reportTestPromise = client.addTestRun('failed', {
145
165
  ...stripExampleFromTitle(test.title),
146
166
  rid: test.uid,
147
167
  test_id: getTestomatIdFromTestTitle(test.title),
@@ -149,6 +169,7 @@ function CodeceptReporter(config) {
149
169
  error,
150
170
  time: hook?.runnable?.duration,
151
171
  });
172
+ reportTestPromises.push(reportTestPromise);
152
173
  }
153
174
  });
154
175
 
@@ -170,15 +191,10 @@ function CodeceptReporter(config) {
170
191
  testTimeMap[test.uid] = Date.now();
171
192
  });
172
193
 
173
- event.dispatcher.on(event.all.result, async result => {
174
- debug('waiting for all tests to be reported');
175
- // all tests were reported and we can upload videos
176
- await Promise.all(reportTestPromises);
177
-
178
- await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
179
- await uploadAttachments(client, traces, '📁 Uploading', 'trace');
180
-
181
- client.updateRunStatus('finished');
194
+ event.dispatcher.on(event.all.after, () => {
195
+ recorder.add('Finishing run', async () => {
196
+ await finalizeRun('all.after');
197
+ });
182
198
  });
183
199
 
184
200
  event.dispatcher.on(event.test.after, test => {
@@ -190,12 +206,19 @@ function CodeceptReporter(config) {
190
206
  const logs = getTestLogs(test);
191
207
  const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
192
208
  const keyValues = services.keyValues.get(test.fullTitle());
193
- const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
194
209
  const links = services.links.get(test.fullTitle());
210
+ const screenshotOnFailPath = artifacts.screenshot || null;
211
+
212
+ // Build step hierarchy with screenshot from screenshotOnFail
213
+ const stepHierarchy = buildUnifiedStepHierarchy(
214
+ test.steps,
215
+ hookSteps,
216
+ screenshotOnFailPath
217
+ );
195
218
 
196
219
  services.setContext(null);
197
220
 
198
- client.addTestRun(test.state, {
221
+ const reportTestPromise = client.addTestRun(test.state, {
199
222
  ...stripExampleFromTitle(title),
200
223
  rid: uid,
201
224
  test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
@@ -210,6 +233,7 @@ function CodeceptReporter(config) {
210
233
  manuallyAttachedArtifacts,
211
234
  meta: { ...keyValues, ...test.meta },
212
235
  });
236
+ reportTestPromises.push(reportTestPromise);
213
237
 
214
238
  processArtifactsForUpload(artifacts, uid, title, videos, traces);
215
239
  });
@@ -229,7 +253,7 @@ async function uploadAttachments(client, attachments, messagePrefix, attachmentT
229
253
  if (!attachments?.length) return;
230
254
 
231
255
  if (client.uploader.isEnabled) {
232
- console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
256
+ log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
233
257
  }
234
258
 
235
259
  const promises = attachments.map(async attachment => {
@@ -372,7 +396,7 @@ function getTestLogs(test) {
372
396
  }
373
397
 
374
398
  // Build step hierarchy using CodeceptJS built-in methods
375
- function buildUnifiedStepHierarchy(steps, hookSteps) {
399
+ function buildUnifiedStepHierarchy(steps, hookSteps, screenshotOnFailPath = null) {
376
400
  const hierarchy = [];
377
401
 
378
402
  // Add pre-test hooks
@@ -380,7 +404,7 @@ function buildUnifiedStepHierarchy(steps, hookSteps) {
380
404
 
381
405
  // Process test steps if they exist
382
406
  if (steps && steps.length > 0) {
383
- processTestSteps(steps, hierarchy);
407
+ processTestSteps(steps, hierarchy, screenshotOnFailPath);
384
408
  }
385
409
 
386
410
  // Add post-test hooks
@@ -398,11 +422,18 @@ function addHooksToHierarchy(hierarchy, hookSteps, hookNames) {
398
422
  }
399
423
  }
400
424
 
401
- function processTestSteps(steps, hierarchy) {
425
+ function processTestSteps(steps, hierarchy, screenshotOnFailPath = null) {
402
426
  const sectionMap = new Map();
427
+ let screenshotAttached = false;
403
428
 
404
429
  for (const step of steps) {
405
- const formattedStep = formatCodeceptStep(step);
430
+ let stepScreenshotPath = null;
431
+ if (screenshotOnFailPath && !screenshotAttached && step.status === 'failed') {
432
+ stepScreenshotPath = screenshotOnFailPath;
433
+ screenshotAttached = true;
434
+ }
435
+
436
+ const formattedStep = formatCodeceptStep(step, stepScreenshotPath);
406
437
  if (!formattedStep) continue;
407
438
 
408
439
  if (step.metaStep) {
@@ -460,27 +491,45 @@ function formatHookName(hookName) {
460
491
  }
461
492
 
462
493
  // Format CodeceptJS step using its built-in methods
463
- function formatCodeceptStep(step) {
494
+ function formatCodeceptStep(step, screenshotOnFailPath = null) {
464
495
  if (!step) return null;
465
496
 
466
497
  const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
467
- const title = truncate(step); // Use built-in toString
468
- const duration = step.duration || 0; // Use built-in duration
498
+ const title = truncate(String(step));
499
+ const duration = step.duration || 0;
469
500
 
470
- const formattedStep = {
501
+ const formattedStep = formatStep({
471
502
  category,
472
503
  title,
473
504
  duration,
474
- };
505
+ });
506
+
507
+ // Add status
508
+ addStatusToStep(formattedStep, step.status, step.err);
475
509
 
476
510
  // Add error if step failed
477
511
  if (step.status === 'failed' && step.err) {
478
512
  formattedStep.error = {
479
- message: step.err.message || 'Step failed',
480
- stack: step.err.stack || '',
513
+ message: truncate(String(step.err.message || 'Step failed'), 250),
514
+ stack: truncate(String(step.err.stack || ''), 250),
481
515
  };
482
516
  }
483
517
 
518
+ // Add artifacts
519
+ if (step.artifacts && SCREENSHOTS_ON_STEPS) {
520
+ addArtifactsToStep(formattedStep, step.artifacts);
521
+ }
522
+
523
+ // Add screenshot from screenshotOnFail plugin
524
+ if (screenshotOnFailPath && SCREENSHOTS_ON_STEPS) {
525
+ addArtifactPathToStep(formattedStep, screenshotOnFailPath);
526
+ }
527
+
528
+ // Add log if present
529
+ if (step.log) {
530
+ formattedStep.log = truncate(String(step.log), 250);
531
+ }
532
+
484
533
  return formattedStep;
485
534
  }
486
535
 
@@ -492,17 +541,38 @@ function formatHookStep(step) {
492
541
  if (step.actor && step.name) {
493
542
  title = `${step.actor} ${step.name}`;
494
543
  if (step.args && step.args.length > 0) {
495
- const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
544
+ const argsStr = step.args.map(arg => truncate(JSON.stringify(arg), 250)).join(', ');
496
545
  title += ` ${argsStr}`;
497
546
  }
498
547
  }
499
548
  title = truncate(title);
500
549
 
501
- return {
550
+ const formattedStep = formatStep({
502
551
  category: 'hook',
503
552
  title,
504
553
  duration: step.duration || 0,
505
- };
554
+ });
555
+
556
+ addStatusToStep(formattedStep, step.status, step.err);
557
+
558
+ if (step.status === 'failed' && step.err) {
559
+ formattedStep.error = {
560
+ message: truncate(String(step.err.message || 'Hook failed'), 250),
561
+ stack: truncate(String(step.err.stack || ''), 250),
562
+ };
563
+ }
564
+
565
+ // Add artifacts
566
+ if (step.artifacts && SCREENSHOTS_ON_STEPS) {
567
+ addArtifactsToStep(formattedStep, step.artifacts);
568
+ }
569
+
570
+ // Add log if present
571
+ if (step.log) {
572
+ formattedStep.log = truncate(String(step.log), 250);
573
+ }
574
+
575
+ return formattedStep;
506
576
  }
507
577
 
508
578
  export { CodeceptReporter };