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