@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
@@ -4,7 +4,7 @@ import TestomatClient from '../client.js';
4
4
  import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
5
5
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
6
6
  import { services } from '../services/index.js';
7
- // eslint-disable-next-line
7
+ import { dataStorage } from '../data-storage.js';
8
8
  import codeceptjs from 'codeceptjs';
9
9
 
10
10
  const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
@@ -15,19 +15,15 @@ if (!global.codeceptjs) {
15
15
  }
16
16
 
17
17
  // @ts-ignore
18
- const { event, recorder, codecept } = global.codeceptjs;
18
+ const { event, recorder, codecept, output } = global.codeceptjs;
19
19
 
20
- let currentMetaStep = [];
21
- let error;
22
- let stepShift = 0;
20
+ const [, MAJOR_VERSION, MINOR_VERSION] = codecept.version().match(/(\d+)\.(\d+)/).map(Number);
23
21
 
24
- // const output = new Output({
25
- // filterFn: stack => !stack.includes('codeceptjs/lib/output'), // output from codeceptjs
26
- // });
27
-
28
- let stepStart = new Date();
29
-
30
- const MAJOR_VERSION = parseInt(codecept.version().match(/\d/)[0], 10);
22
+ // Constants for hook execution order
23
+ const HOOK_EXECUTION_ORDER = {
24
+ PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
25
+ POST_TEST: ['AfterHook', 'AfterSuiteHook']
26
+ };
31
27
 
32
28
  const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
33
29
 
@@ -35,8 +31,12 @@ if (MAJOR_VERSION < 3) {
35
31
  console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
36
32
  }
37
33
 
34
+ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
35
+ console.log('🔴 CodeceptJS 3.7+ is supported, please upgrade CodeceptJS or use 1.6 version of `@testomatio/reporter`');
36
+ }
37
+
38
38
  function CodeceptReporter(config) {
39
- let failedTests = [];
39
+ const failedTests = [];
40
40
  let videos = [];
41
41
  let traces = [];
42
42
  const reportTestPromises = [];
@@ -44,92 +44,104 @@ function CodeceptReporter(config) {
44
44
  const testTimeMap = {};
45
45
  const { apiKey } = config;
46
46
 
47
- const getDuration = test => {
48
- if (!test.uid) return 0;
49
- if (testTimeMap[test.uid]) {
50
- return Date.now() - testTimeMap[test.uid];
51
- }
47
+ const client = new TestomatClient({ apiKey });
52
48
 
53
- return 0;
49
+ // Store original output methods for fallback
50
+ const originalOutput = {
51
+ debug: output.debug,
52
+ log: output.log,
53
+ step: output.step,
54
+ say: output.say,
54
55
  };
55
56
 
56
- const client = new TestomatClient({ apiKey });
57
+ output.debug = function(msg) {
58
+ originalOutput.debug(msg);
59
+ dataStorage.putData('log', repeat(this.stepShift) + pc.cyan(msg.toString()));
60
+ };
61
+
62
+ output.say = function(message, color = 'cyan') {
63
+ originalOutput.say(message, color);
64
+ const sayMsg = repeat(this.stepShift) + ` ${pc.bold(pc[color](message))}`;
65
+ dataStorage.putData('log', sayMsg);
66
+ };
67
+
68
+ output.log = function(msg) {
69
+ originalOutput.log(msg);
70
+ dataStorage.putData('log', repeat(this.stepShift) + pc.gray(msg));
71
+ };
57
72
 
58
73
  recorder.startUnlessRunning();
59
74
 
75
+ const hookSteps = new Map();
76
+ let currentHook = null;
77
+
78
+ event.dispatcher.on(event.workers.before, () => {
79
+ recorder.add('Creating new run', async () => {
80
+ await client.createRun();
81
+ process.env.TESTOMATIO_RUN = client.runId;
82
+ process.env.TESTOMATIO_PROCEED = 'true';
83
+ debug('Run ID:', client.runId);
84
+ });
85
+ });
86
+
87
+ event.dispatcher.on(event.workers.after, () => {
88
+ client.updateRunStatus('finished');
89
+ });
90
+
60
91
  // Listening to events
61
92
  event.dispatcher.on(event.all.before, () => {
62
93
  // clear tmp dir
63
- fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
94
+ // fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
64
95
 
65
96
  // recorder.add('Creating new run', () => );
66
- client.createRun();
97
+ recorder.add('Creating new run', () => {
98
+ return client.createRun();
99
+ });
67
100
  videos = [];
68
101
  traces = [];
69
102
 
70
103
  if (!global.testomatioDataStore) global.testomatioDataStore = {};
71
104
  });
72
105
 
73
- let hookSteps = [];
74
- let suiteHookRunning = false;
75
-
76
- event.dispatcher.on(event.suite.before, suite => {
77
- suiteHookRunning = true;
78
- hookSteps = [];
79
- global.testomatioDataStore.steps = [];
80
-
81
- services.setContext(suite.fullTitle());
106
+ // Hook event listeners
107
+ event.dispatcher.on(event.hook.started, (hook) => {
108
+ output.stepShift = 2;
109
+ currentHook = hook.name;
110
+ let title = hook.hookName;
111
+ if (hook.suite) title += ' ' + hook.suite.fullTitle();
112
+ if (hook.test) title += ' ' + hook.test.fullTitle();
113
+ if (hook.ctx.currentTest) title += ' ' + hook.ctx.currentTest.fullTitle();
114
+
115
+ services.setContext(title);
116
+ hookSteps.set(hook.name, []);
82
117
  });
83
118
 
84
- event.dispatcher.on(event.suite.after, () => {
119
+ event.dispatcher.on(event.hook.finished, () => {
120
+ currentHook = null;
121
+ output.stepShift = 2;
85
122
  services.setContext(null);
86
123
  });
87
124
 
88
- event.dispatcher.on(event.hook.started, () => {
89
- // global.testomatioDataStore.steps = [];
90
- });
91
125
 
92
- event.dispatcher.on(event.hook.passed, () => {
93
- if (suiteHookRunning) {
94
- hookSteps.push(...global.testomatioDataStore.steps);
95
- services.setContext(null);
96
- }
126
+ event.dispatcher.on(event.suite.before, suite => {
127
+ dataStorage.setContext(suite.fullTitle());
97
128
  });
98
129
 
99
- event.dispatcher.on(event.hook.failed, () => {
100
- if (suiteHookRunning) {
101
- hookSteps.push(...global.testomatioDataStore.steps);
102
- services.setContext(null);
103
- }
130
+ event.dispatcher.on(event.suite.after, () => {
131
+ services.setContext(null);
104
132
  });
105
133
 
106
134
  event.dispatcher.on(event.test.before, test => {
107
- suiteHookRunning = false;
108
- global.testomatioDataStore.steps = [];
109
-
110
- recorder.add(() => {
111
- currentMetaStep = [];
112
- // output.reset();
113
- // output.start();
114
- stepShift = 0;
115
- });
116
-
117
- if (!global.testomatioDataStore) global.testomatioDataStore = {};
118
- // reset steps
119
- global.testomatioDataStore.steps = [];
120
-
135
+ initializeTestDataStore();
121
136
  services.setContext(test.fullTitle());
122
137
  });
123
138
 
124
139
  event.dispatcher.on(event.test.started, test => {
125
140
  services.setContext(test.fullTitle());
126
-
127
- testTimeMap[test.id] = Date.now();
128
- if (!test.uid) return;
129
141
  testTimeMap[test.uid] = Date.now();
130
142
  });
131
143
 
132
- event.dispatcher.on(event.all.result, async () => {
144
+ event.dispatcher.on(event.all.result, async (result) => {
133
145
  debug('waiting for all tests to be reported');
134
146
  // all tests were reported and we can upload videos
135
147
  await Promise.all(reportTestPromises);
@@ -137,174 +149,50 @@ function CodeceptReporter(config) {
137
149
  await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
138
150
  await uploadAttachments(client, traces, '📁 Uploading', 'trace');
139
151
 
140
- const status = failedTests.length === 0 ? STATUS.PASSED : STATUS.FAILED;
141
- // @ts-ignore
142
- client.updateRunStatus(status);
143
- });
144
-
145
- event.dispatcher.on(event.test.passed, test => {
146
- const { uid, tags, title } = test;
147
- if (uid && failedTests.includes(uid)) {
148
- failedTests = failedTests.filter(failed => uid !== failed);
149
- }
150
- const testObj = getTestAndMessage(title);
151
-
152
- const logs = getTestLogs(test);
153
- const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
154
- const keyValues = services.keyValues.get(test.fullTitle());
155
- services.setContext(null);
156
-
157
- client.addTestRun(STATUS.PASSED, {
158
- ...stripExampleFromTitle(title),
159
- rid: uid,
160
- suite_title: test.parent && test.parent.title,
161
- message: testObj.message,
162
- time: getDuration(test),
163
- steps: global.testomatioDataStore.steps.join('\n') || null,
164
- test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
165
- logs,
166
- manuallyAttachedArtifacts,
167
- meta: keyValues,
168
- });
169
- // output.stop();
170
- });
171
-
172
- event.dispatcher.on(event.test.failed, (test, err) => {
173
- error = err;
174
- });
175
-
176
- event.dispatcher.on(event.hook.failed, (suite, err) => {
177
- error = err;
178
-
179
- if (!suite) return;
180
- if (!suite.tests) return;
181
- for (const test of suite.tests) {
182
- const { uid, tags, title } = test;
183
- failedTests.push(uid || title);
184
- const testId = getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`);
185
-
186
- client.addTestRun(STATUS.FAILED, {
187
- rid: uid,
188
- ...stripExampleFromTitle(title),
189
- suite_title: suite.title,
190
- test_id: testId,
191
- error,
192
- time: 0,
193
- });
194
- }
195
- // output.stop();
152
+ client.updateRunStatus('finished');
196
153
  });
197
154
 
198
155
  event.dispatcher.on(event.test.after, test => {
199
- if (test.state && test.state !== STATUS.FAILED) return;
200
- if (test.err) error = test.err;
201
- const { uid, tags, title, artifacts } = test;
156
+ const { uid, tags, title, artifacts } = test.simplify();
157
+ const error = test.err || null;
202
158
  failedTests.push(uid || title);
203
159
  const testObj = getTestAndMessage(title);
204
-
205
- const files = [];
206
- if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
207
- // todo: video must be uploaded later....
208
-
160
+ const files = buildArtifactFiles(artifacts);
209
161
  const logs = getTestLogs(test);
210
162
  const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
211
163
  const keyValues = services.keyValues.get(test.fullTitle());
164
+ const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
165
+ const labels = services.labels.get(test.fullTitle());
166
+
212
167
  services.setContext(null);
213
168
 
214
- client.addTestRun(STATUS.FAILED, {
169
+ client.addTestRun(test.state, {
215
170
  ...stripExampleFromTitle(title),
216
171
  rid: uid,
217
172
  test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
218
- suite_title: test.parent && test.parent.title,
173
+ suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
219
174
  error,
220
175
  message: testObj.message,
221
- time: getDuration(test),
176
+ time: test.duration,
222
177
  files,
223
- steps: global.testomatioDataStore?.steps?.join('\n') || null,
178
+ steps: stepHierarchy, // Array of step objects per API schema
224
179
  logs,
180
+ labels,
225
181
  manuallyAttachedArtifacts,
226
- meta: keyValues,
182
+ meta: { ...keyValues, ...test.meta },
227
183
  });
228
184
 
229
- debug('artifacts', artifacts);
230
-
231
- for (const aid in artifacts) {
232
- if (aid.startsWith('video')) videos.push({ rid: uid, title, path: artifacts[aid], type: 'video/webm' });
233
- if (aid.startsWith('trace')) traces.push({ rid: uid, title, path: artifacts[aid], type: 'application/zip' });
234
- }
235
-
236
- // output.stop();
237
- });
238
-
239
- event.dispatcher.on(event.test.skipped, test => {
240
- const { uid, tags, title } = test;
241
- if (failedTests.includes(uid || title)) return;
242
-
243
- const testObj = getTestAndMessage(title);
244
- client.addTestRun(STATUS.SKIPPED, {
245
- rid: uid,
246
- ...stripExampleFromTitle(title),
247
- test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
248
- suite_title: test.parent && test.parent.title,
249
- message: testObj.message,
250
- time: getDuration(test),
251
- });
252
- // output.stop();
185
+ processArtifactsForUpload(artifacts, uid, title, videos, traces);
253
186
  });
254
187
 
255
188
  event.dispatcher.on(event.step.started, step => {
256
- stepShift = 0;
257
- step.started = true;
258
- stepStart = new Date();
189
+ const stepText = `${repeat(output.stepShift)} ${step.toCliStyled ? step.toCliStyled() : step.toString()}`;
190
+ dataStorage.putData('log', stepText);
259
191
  });
260
192
 
261
193
  event.dispatcher.on(event.step.finished, step => {
262
- if (!step.started) return;
263
- let processingStep = step;
264
- const metaSteps = [];
265
- while (processingStep.metaStep) {
266
- metaSteps.unshift(processingStep.metaStep);
267
- processingStep = processingStep.metaStep;
268
- }
269
- const shift = metaSteps.length;
270
-
271
- for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
272
- if (currentMetaStep[i] !== metaSteps[i]) {
273
- stepShift = 2 * i;
274
- // eslint-disable-next-line no-continue
275
- if (!metaSteps[i]) continue;
276
- if (metaSteps[i].isBDD()) {
277
- // output.push(repeat(stepShift) + pc.bold(metaSteps[i].toString()) + metaSteps[i].comment);
278
- global.testomatioDataStore?.steps?.push(
279
- repeat(stepShift) + pc.bold(metaSteps[i].toString()) + metaSteps[i].comment,
280
- );
281
- } else {
282
- // output.push(repeat(stepShift) + pc.green.bold(metaSteps[i].toString()));
283
- global.testomatioDataStore?.steps?.push(repeat(stepShift) + pc.green(pc.bold(metaSteps[i].toString())));
284
- }
285
- }
286
- }
287
- currentMetaStep = metaSteps;
288
- stepShift = 2 * shift;
289
-
290
- const durationMs = +new Date() - +stepStart;
291
- let duration = '';
292
- if (durationMs) {
293
- duration = repeat(1) + pc.gray(`(${durationMs}ms)`);
294
- }
295
-
296
- if (step.status === STATUS.FAILED) {
297
- // output.push(repeat(stepShift) + pc.red(step.toString()) + duration);
298
- global.testomatioDataStore?.steps?.push(repeat(stepShift) + pc.red(step.toString()) + duration);
299
- } else {
300
- // output.push(repeat(stepShift) + step.toString() + duration);
301
- global.testomatioDataStore?.steps?.push(repeat(stepShift) + step.toString() + duration);
302
- }
303
- });
304
-
305
- event.dispatcher.on(event.step.comment, step => {
306
- // output.push(pc.cyan.bold(step.toString()));
307
- global.testomatioDataStore?.steps?.push(pc.cyan(pc.bold(step.toString())));
194
+ processMetaStepsForDisplay(step);
195
+ captureHookStep(step, currentHook, hookSteps);
308
196
  });
309
197
  }
310
198
 
@@ -344,32 +232,250 @@ function stripExampleFromTitle(title) {
344
232
  const res = title.match(DATA_REGEXP);
345
233
  if (!res) return { title, example: null };
346
234
 
347
- const example = JSON.parse(res[1]);
348
- title = title.replace(DATA_REGEXP, '').trim();
235
+ try {
236
+ const example = JSON.parse(res[1]);
237
+ title = title.replace(DATA_REGEXP, '').trim();
238
+ return { title, example };
239
+ } catch (e) {
240
+ // If JSON parsing fails, return title without example
241
+ debug('Failed to parse example JSON:', res[1], e.message);
242
+ return { title: title.replace(DATA_REGEXP, '').trim(), example: null };
243
+ }
244
+ }
349
245
 
350
- return { title, example };
246
+ function stripTagsFromTitle(title) {
247
+ // Remove @tags from the end of titles (e.g., "Hooks Test Suite @hooks" -> "Hooks Test Suite")
248
+ return title.replace(/\s+@[\w-]+\s*$/, '').trim();
351
249
  }
352
250
 
353
251
  function repeat(num) {
354
252
  return ''.padStart(num, ' ');
355
253
  }
356
254
 
255
+ // Helper functions for cleaner event handling
256
+ function initializeTestDataStore() {
257
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
258
+ global.testomatioDataStore.steps = [];
259
+ }
260
+
261
+ function buildArtifactFiles(artifacts) {
262
+ const files = [];
263
+ if (artifacts.screenshot) {
264
+ files.push({ path: artifacts.screenshot, type: 'image/png' });
265
+ }
266
+ return files;
267
+ }
268
+
269
+ function processArtifactsForUpload(artifacts, uid, title, videos, traces) {
270
+ for (const aid in artifacts) {
271
+ if (aid.startsWith('video')) {
272
+ videos.push({ rid: uid, title, path: artifacts[aid], type: 'video/webm' });
273
+ }
274
+ if (aid.startsWith('trace')) {
275
+ traces.push({ rid: uid, title, path: artifacts[aid], type: 'application/zip' });
276
+ }
277
+ }
278
+ }
279
+
280
+ function processMetaStepsForDisplay(step) {
281
+ const metaSteps = [];
282
+ let processingStep = step;
283
+
284
+ while (processingStep.metaStep) {
285
+ metaSteps.unshift(processingStep.metaStep);
286
+ processingStep = processingStep.metaStep;
287
+ }
288
+ }
289
+
290
+ function captureHookStep(step, currentHook, hookSteps) {
291
+ if (!currentHook) return;
292
+
293
+ const startTime = step.startTime;
294
+ const endTime = step.endTime;
295
+
296
+ const hookStepsArray = hookSteps.get(currentHook) || [];
297
+ hookStepsArray.push({
298
+ name: step.name,
299
+ actor: step.actor,
300
+ args: step.args,
301
+ status: step.status,
302
+ startTime,
303
+ endTime,
304
+ helperMethod: step.helperMethod
305
+ });
306
+ hookSteps.set(currentHook, hookStepsArray);
307
+ }
308
+
357
309
  // TODO: think about moving to some common utils
358
310
  function getTestLogs(test) {
359
- const suiteLogsArr = services.logger.getLogs(test.parent.fullTitle());
360
- const suiteLogs = suiteLogsArr ? suiteLogsArr.join('\n').trim() : '';
361
- const testLogsArr = services.logger.getLogs(test.fullTitle());
311
+ // Contexts for each log section
312
+ const suiteTitle = test.parent.fullTitle();
313
+ const testTitle = test.fullTitle();
314
+ const beforeSuiteLogsArr = services.logger.getLogs(`BeforeSuite ${suiteTitle}`);
315
+ const beforeLogsArr = services.logger.getLogs(`Before ${testTitle}`);
316
+ const testLogsArr = services.logger.getLogs(testTitle);
317
+ const afterLogsArr = services.logger.getLogs(`After ${testTitle}`);
318
+ const afterSuiteLogsArr = services.logger.getLogs(`AfterSuite ${suiteTitle}`);
319
+
320
+ const beforeSuiteLogs = beforeSuiteLogsArr ? beforeSuiteLogsArr.join('\n').trim() : '';
321
+ const beforeLogs = beforeLogsArr ? beforeLogsArr.join('\n').trim() : '';
362
322
  const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
323
+ const afterLogs = afterLogsArr ? afterLogsArr.join('\n').trim() : '';
324
+ const afterSuiteLogs = afterSuiteLogsArr ? afterSuiteLogsArr.join('\n').trim() : '';
363
325
 
364
326
  let logs = '';
365
- if (suiteLogs) {
366
- logs += `${pc.bold('\t--- BeforeSuite ---')}\n${suiteLogs}`;
327
+ if (beforeSuiteLogs) {
328
+ logs += `${pc.bold('--- BeforeSuite ---')}\n${beforeSuiteLogs}`;
329
+ }
330
+ if (beforeLogs) {
331
+ logs += `\n${pc.bold('--- Before ---')}\n${beforeLogs}`;
367
332
  }
368
333
  if (testLogs) {
369
- logs += `\n${pc.bold('\t--- Test ---')}\n${testLogs}`;
334
+ logs += `\n${pc.bold('--- Test ---')}\n${testLogs}`;
335
+ }
336
+ if (afterLogs) {
337
+ logs += `\n${pc.bold('--- After ---')}\n${afterLogs}`;
338
+ }
339
+ if (afterSuiteLogs) {
340
+ logs += `\n${pc.bold('--- AfterSuite ---')}\n${afterSuiteLogs}`;
370
341
  }
371
342
  return logs;
372
343
  }
373
344
 
345
+ // Build step hierarchy using CodeceptJS built-in methods
346
+ function buildUnifiedStepHierarchy(steps, hookSteps) {
347
+ const hierarchy = [];
348
+
349
+ // Add pre-test hooks
350
+ addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.PRE_TEST);
351
+
352
+ // Process test steps if they exist
353
+ if (steps && steps.length > 0) {
354
+ processTestSteps(steps, hierarchy);
355
+ }
356
+
357
+ // Add post-test hooks
358
+ addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.POST_TEST);
359
+
360
+ return hierarchy;
361
+ }
362
+
363
+ function addHooksToHierarchy(hierarchy, hookSteps, hookNames) {
364
+ for (const hookName of hookNames) {
365
+ if (hookSteps.has(hookName)) {
366
+ const hookSection = createHookSection(hookName, hookSteps.get(hookName));
367
+ if (hookSection) hierarchy.push(hookSection);
368
+ }
369
+ }
370
+ }
371
+
372
+ function processTestSteps(steps, hierarchy) {
373
+ const sectionMap = new Map();
374
+
375
+ for (const step of steps) {
376
+ const formattedStep = formatCodeceptStep(step);
377
+ if (!formattedStep) continue;
378
+
379
+ if (step.metaStep) {
380
+ // Step belongs to a section (meta step)
381
+ const sectionKey = step.metaStep;
382
+ let sectionStep = sectionMap.get(sectionKey);
383
+
384
+ if (!sectionStep) {
385
+ sectionStep = createSectionStep(step.metaStep);
386
+ sectionMap.set(sectionKey, sectionStep);
387
+ hierarchy.push(sectionStep);
388
+ }
389
+
390
+ sectionStep.steps.push(formattedStep);
391
+ sectionStep.duration += formattedStep.duration || 0;
392
+ } else {
393
+ // Regular step
394
+ hierarchy.push(formattedStep);
395
+ }
396
+ }
397
+ }
398
+
399
+
400
+ function createSectionStep(metaStep) {
401
+ return {
402
+ category: 'user',
403
+ title: metaStep.toString(), // Use built-in toString method
404
+ duration: metaStep.duration || 0, // Use built-in duration
405
+ steps: []
406
+ };
407
+ }
408
+
409
+ function createHookSection(hookName, steps) {
410
+ if (!steps || steps.length === 0) return null;
411
+
412
+ const hookSection = {
413
+ category: 'hook',
414
+ title: formatHookName(hookName),
415
+ duration: 0,
416
+ steps: []
417
+ };
418
+
419
+ for (const step of steps) {
420
+ const formattedStep = formatHookStep(step);
421
+ if (formattedStep) {
422
+ hookSection.steps.push(formattedStep);
423
+ hookSection.duration += formattedStep.duration || 0;
424
+ }
425
+ }
426
+
427
+ return hookSection.steps.length > 0 ? hookSection : null;
428
+ }
429
+
430
+ function formatHookName(hookName) {
431
+ return hookName.replace(/Hook$/, '');
432
+ }
433
+
434
+
435
+ // Format CodeceptJS step using its built-in methods
436
+ function formatCodeceptStep(step) {
437
+ if (!step) return null;
438
+
439
+ const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
440
+ const title = step.toString(); // Use built-in toString
441
+ const duration = step.duration || 0; // Use built-in duration
442
+
443
+ const formattedStep = {
444
+ category,
445
+ title,
446
+ duration
447
+ };
448
+
449
+ // Add error if step failed
450
+ if (step.status === 'failed' && step.err) {
451
+ formattedStep.error = {
452
+ message: step.err.message || 'Step failed',
453
+ stack: step.err.stack || ''
454
+ };
455
+ }
456
+
457
+ return formattedStep;
458
+ }
459
+
460
+ function formatHookStep(step) {
461
+ if (!step) return null;
462
+
463
+ // For hook steps, construct title from available properties
464
+ let title = step.name;
465
+ if (step.actor && step.name) {
466
+ title = `${step.actor}.${step.name}`;
467
+ if (step.args && step.args.length > 0) {
468
+ const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
469
+ title += `(${argsStr})`;
470
+ }
471
+ }
472
+
473
+ return {
474
+ category: 'hook',
475
+ title,
476
+ duration: step.duration || 0
477
+ };
478
+ }
479
+
374
480
  export { CodeceptReporter };
375
481
  export default CodeceptReporter;
@@ -44,7 +44,6 @@ const testomatioReporter = on => {
44
44
 
45
45
  if (!error && test.displayError) {
46
46
  error = { message: test.displayError };
47
- // eslint-disable-next-line
48
47
  error.inspect = function () {
49
48
  return this.message;
50
49
  };
@@ -56,7 +55,6 @@ const testomatioReporter = on => {
56
55
  name: error.name,
57
56
  inspect:
58
57
  error.inspect ||
59
- // eslint-disable-next-line
60
58
  function () {
61
59
  return this.message;
62
60
  },
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
1
  import Mocha from 'mocha';
3
2
  import TestomatClient from '../client.js';
4
3
  import { STATUS, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
@@ -1,6 +1,6 @@
1
1
  import TestomatClient from '../client.js';
2
2
  import { config } from '../config.js';
3
- import { STATUS } from '../constants';
3
+ import { STATUS } from '../constants.js';
4
4
  import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
5
 
6
6
  const apiKey = config.TESTOMATIO;