testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn

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 (67) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.js +6 -26
  3. package/package.json +39 -4
  4. package/src/adapter/codecept.js +626 -0
  5. package/src/adapter/cucumber/current.js +230 -0
  6. package/src/adapter/cucumber/legacy.js +158 -0
  7. package/src/adapter/cucumber.js +4 -0
  8. package/src/adapter/cypress-plugin/index.js +110 -0
  9. package/src/adapter/jasmine.js +60 -0
  10. package/src/adapter/jest.js +108 -0
  11. package/src/adapter/mocha.cjs +2 -0
  12. package/src/adapter/mocha.js +211 -0
  13. package/src/adapter/nightwatch.js +88 -0
  14. package/src/adapter/playwright.js +343 -0
  15. package/src/adapter/utils/playwright.js +121 -0
  16. package/src/adapter/utils/step-formatter.js +232 -0
  17. package/src/adapter/vitest.js +455 -0
  18. package/src/adapter/webdriver.js +201 -0
  19. package/src/bin/cli.js +507 -0
  20. package/src/bin/reportXml.js +79 -0
  21. package/src/bin/startTest.js +54 -0
  22. package/src/bin/uploadArtifacts.js +91 -0
  23. package/src/client.js +524 -0
  24. package/src/config.js +30 -0
  25. package/src/constants.js +72 -0
  26. package/src/data-storage.js +204 -0
  27. package/src/helpers.js +1 -0
  28. package/src/junit-adapter/adapter.js +23 -0
  29. package/src/junit-adapter/csharp.js +70 -0
  30. package/src/junit-adapter/index.js +28 -0
  31. package/src/junit-adapter/java.js +58 -0
  32. package/src/junit-adapter/javascript.js +31 -0
  33. package/src/junit-adapter/nunit-parser.js +474 -0
  34. package/src/junit-adapter/python.js +42 -0
  35. package/src/junit-adapter/ruby.js +10 -0
  36. package/src/output.js +57 -0
  37. package/src/pipe/bitbucket.js +285 -0
  38. package/src/pipe/coverage.js +500 -0
  39. package/src/pipe/csv.js +161 -0
  40. package/src/pipe/debug.js +143 -0
  41. package/src/pipe/github.js +256 -0
  42. package/src/pipe/gitlab.js +258 -0
  43. package/src/pipe/html.js +1153 -0
  44. package/src/pipe/index.js +73 -0
  45. package/src/pipe/markdown.js +753 -0
  46. package/src/pipe/testomatio.js +707 -0
  47. package/src/replay.js +274 -0
  48. package/src/reporter-functions.js +155 -0
  49. package/src/reporter.js +42 -0
  50. package/src/services/artifacts.js +59 -0
  51. package/src/services/index.js +15 -0
  52. package/src/services/key-values.js +59 -0
  53. package/src/services/links.js +69 -0
  54. package/src/services/logger.js +320 -0
  55. package/src/template/emptyData.svg +23 -0
  56. package/src/template/testomatio-old.hbs +1421 -0
  57. package/src/template/testomatio.hbs +3726 -0
  58. package/src/uploader.js +382 -0
  59. package/src/utils/constants.js +12 -0
  60. package/src/utils/debug.js +20 -0
  61. package/src/utils/log-formatter.js +118 -0
  62. package/src/utils/log.js +88 -0
  63. package/src/utils/pipe_utils.js +193 -0
  64. package/src/utils/utils.js +732 -0
  65. package/src/xmlReader.js +834 -0
  66. package/types/types.d.ts +425 -0
  67. package/types/vitest.types.d.ts +93 -0
@@ -0,0 +1,626 @@
1
+ import createDebugMessages from 'debug';
2
+ import path from 'path';
3
+ import pc from 'picocolors';
4
+ import TestomatClient from '../client.js';
5
+ import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR, SCREENSHOTS_ON_STEPS } from '../constants.js';
6
+ import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
7
+ import { services } from '../services/index.js';
8
+ import { dataStorage } from '../data-storage.js';
9
+ import { formatStep, addStatusToStep, addArtifactsToStep, addArtifactPathToStep } from './utils/step-formatter.js';
10
+ import codeceptjs from 'codeceptjs';
11
+ import { log } from '../utils/log.js';
12
+
13
+ const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
14
+ // @ts-ignore
15
+ if (!global.codeceptjs) {
16
+ // @ts-ignore
17
+ global.codeceptjs = codeceptjs;
18
+ }
19
+
20
+ // @ts-ignore
21
+ const { event, recorder, codecept, output } = global.codeceptjs;
22
+
23
+ const [, MAJOR_VERSION, MINOR_VERSION] = codecept
24
+ .version()
25
+ .match(/(\d+)\.(\d+)/)
26
+ .map(Number);
27
+
28
+ // Constants for hook execution order
29
+ const HOOK_EXECUTION_ORDER = {
30
+ PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
31
+ POST_TEST: ['AfterHook', 'AfterSuiteHook'],
32
+ };
33
+
34
+ // codeceptjs workers are self-contained
35
+ dataStorage.isFileStorage = false;
36
+
37
+ const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
38
+
39
+ if (MAJOR_VERSION < 3) {
40
+ console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
41
+ }
42
+
43
+ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
44
+ console.log(
45
+ '🔴 CodeceptJS 3.7+ is supported, please upgrade CodeceptJS or use 1.6 version of `@testomatio/reporter`',
46
+ );
47
+ }
48
+
49
+ function CodeceptReporter(config) {
50
+ const failedTests = [];
51
+ let videos = [];
52
+ let traces = [];
53
+ const reportTestPromises = [];
54
+ let isRunFinalized = false;
55
+
56
+ const testTimeMap = {};
57
+ const clientConfig = buildCodeceptClientConfig(config);
58
+ const client = new TestomatClient(clientConfig);
59
+
60
+ // Store original output methods for fallback
61
+ const originalOutput = {
62
+ debug: output.debug,
63
+ log: output.log,
64
+ step: output.step,
65
+ say: output.say,
66
+ };
67
+
68
+ output.debug = function (msg) {
69
+ originalOutput.debug(msg);
70
+ dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.cyan(msg.toString()));
71
+ };
72
+
73
+ output.say = function (message, color = 'cyan') {
74
+ originalOutput.say(message, color);
75
+ const sayMsg = repeat(this?.stepShift || 0) + ` ${pc.bold(pc[color](message))}`;
76
+ dataStorage.putData('log', sayMsg);
77
+ };
78
+
79
+ output.log = function (msg) {
80
+ originalOutput.log(msg);
81
+ dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.gray(msg));
82
+ };
83
+ output.stepShift = 0;
84
+
85
+ recorder.startUnlessRunning();
86
+
87
+ const hookSteps = new Map();
88
+ let currentHook = null;
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
+
103
+ event.dispatcher.on(event.workers.before, () => {
104
+ recorder.add('Creating new run', async () => {
105
+ await client.createRun();
106
+ process.env.TESTOMATIO_RUN = client.runId;
107
+ process.env.TESTOMATIO_PROCEED = 'true';
108
+ debug('Run ID:', client.runId);
109
+ });
110
+ });
111
+
112
+ event.dispatcher.on(event.workers.after, () => {
113
+ recorder.add('Finishing run', async () => {
114
+ await finalizeRun('workers.after');
115
+ });
116
+ });
117
+
118
+ // Listening to events
119
+ event.dispatcher.on(event.all.before, () => {
120
+ // clear tmp dir
121
+ // fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
122
+
123
+ // recorder.add('Creating new run', () => );
124
+ recorder.add('Creating new run', () => {
125
+ return client.createRun();
126
+ });
127
+ videos = [];
128
+ traces = [];
129
+ isRunFinalized = false;
130
+ reportTestPromises.length = 0;
131
+
132
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
133
+ });
134
+
135
+ // Hook event listeners
136
+ event.dispatcher.on(event.hook.started, hook => {
137
+ output.stepShift = 2;
138
+ currentHook = hook.name;
139
+ let title = hook.hookName;
140
+ if (hook.suite) title += ' ' + hook.suite.fullTitle();
141
+ if (hook.test) title += ' ' + hook.test.fullTitle();
142
+ if (hook.ctx.currentTest) title += ' ' + hook.ctx.currentTest.fullTitle();
143
+
144
+ services.setContext(title);
145
+ hookSteps.set(hook.name, []);
146
+ });
147
+
148
+ event.dispatcher.on(event.hook.finished, () => {
149
+ currentHook = null;
150
+ output.stepShift = 2;
151
+ services.setContext(null);
152
+ });
153
+
154
+ // mark as failed all tests inside the failed hook
155
+ event.dispatcher.on(event.hook.failed, hook => {
156
+ if (hook.name !== 'BeforeSuiteHook' && hook.name !== 'BeforeHook') return;
157
+ const suite = hook.runnable.parent;
158
+
159
+ if (!suite) return;
160
+
161
+ const error = hook?.ctx?.currentTest?.err;
162
+
163
+ for (const test of suite.tests) {
164
+ const reportTestPromise = client.addTestRun('failed', {
165
+ ...stripExampleFromTitle(test.title),
166
+ rid: test.uid,
167
+ test_id: getTestomatIdFromTestTitle(test.title),
168
+ suite_title: stripTagsFromTitle(suite.title),
169
+ error,
170
+ time: hook?.runnable?.duration,
171
+ });
172
+ reportTestPromises.push(reportTestPromise);
173
+ }
174
+ });
175
+
176
+ event.dispatcher.on(event.suite.before, suite => {
177
+ dataStorage.setContext(suite.fullTitle());
178
+ });
179
+
180
+ event.dispatcher.on(event.suite.after, () => {
181
+ services.setContext(null);
182
+ });
183
+
184
+ event.dispatcher.on(event.test.before, test => {
185
+ initializeTestDataStore();
186
+ services.setContext(test.fullTitle());
187
+ });
188
+
189
+ event.dispatcher.on(event.test.started, test => {
190
+ services.setContext(test.fullTitle());
191
+ testTimeMap[test.uid] = Date.now();
192
+ });
193
+
194
+ event.dispatcher.on(event.all.after, () => {
195
+ recorder.add('Finishing run', async () => {
196
+ await finalizeRun('all.after');
197
+ });
198
+ });
199
+
200
+ event.dispatcher.on(event.test.after, test => {
201
+ const { uid, tags, title, artifacts } = test.simplify();
202
+ const error = test.err || null;
203
+ failedTests.push(uid || title);
204
+ const testObj = getTestAndMessage(title);
205
+ const files = buildArtifactFiles(artifacts);
206
+ const logs = getTestLogs(test);
207
+ const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
208
+ const keyValues = services.keyValues.get(test.fullTitle());
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
+ );
218
+
219
+ services.setContext(null);
220
+
221
+ const reportTestPromise = client.addTestRun(test.state, {
222
+ ...stripExampleFromTitle(title),
223
+ rid: uid,
224
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
225
+ suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
226
+ error,
227
+ message: testObj.message,
228
+ time: test.duration,
229
+ files,
230
+ steps: stepHierarchy, // Array of step objects per API schema
231
+ logs,
232
+ links,
233
+ manuallyAttachedArtifacts,
234
+ meta: { ...keyValues, ...test.meta },
235
+ });
236
+ reportTestPromises.push(reportTestPromise);
237
+
238
+ processArtifactsForUpload(artifacts, uid, title, videos, traces);
239
+ });
240
+
241
+ event.dispatcher.on(event.step.started, step => {
242
+ const stepText = `${repeat(output.stepShift)} ${step.toCliStyled ? step.toCliStyled() : step.toString()}`;
243
+ dataStorage.putData('log', stepText);
244
+ });
245
+
246
+ event.dispatcher.on(event.step.finished, step => {
247
+ processMetaStepsForDisplay(step);
248
+ captureHookStep(step, currentHook, hookSteps);
249
+ });
250
+ }
251
+
252
+ async function uploadAttachments(client, attachments, messagePrefix, attachmentType) {
253
+ if (!attachments?.length) return;
254
+
255
+ if (client.uploader.isEnabled) {
256
+ log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
257
+ }
258
+
259
+ const promises = attachments.map(async attachment => {
260
+ const { rid, title, path, type } = attachment;
261
+ const file = { path, type, title };
262
+
263
+ // we are storing file if upload is disabled
264
+ if (!client.uploader.isEnabled) return client.uploader.storeUploadedFile(path, client.runId, rid, false);
265
+
266
+ return client.addTestRun(undefined, {
267
+ ...stripExampleFromTitle(title),
268
+ rid,
269
+ files: [file],
270
+ });
271
+ });
272
+
273
+ await Promise.all(promises);
274
+ }
275
+
276
+ function getTestAndMessage(title) {
277
+ const testObj = { message: '' };
278
+ const testArr = title.split(/\s(\|\s\{.*?\})/);
279
+ testObj.title = testArr[0];
280
+
281
+ return testObj;
282
+ }
283
+
284
+ function stripExampleFromTitle(title) {
285
+ const res = title.match(DATA_REGEXP);
286
+ if (!res) return { title, example: null };
287
+
288
+ try {
289
+ const example = JSON.parse(res[1]);
290
+ title = title.replace(DATA_REGEXP, '').trim();
291
+ return { title, example };
292
+ } catch (e) {
293
+ // If JSON parsing fails, return title without example
294
+ debug('Failed to parse example JSON:', res[1], e.message);
295
+ return { title: title.replace(DATA_REGEXP, '').trim(), example: null };
296
+ }
297
+ }
298
+
299
+ function stripTagsFromTitle(title) {
300
+ // Remove @tags from the end of titles (e.g., "Hooks Test Suite @hooks" -> "Hooks Test Suite")
301
+ return title.replace(/\s+@[\w-]+\s*$/, '').trim();
302
+ }
303
+
304
+ function repeat(num) {
305
+ return ''.padStart(num, ' ');
306
+ }
307
+
308
+ // Helper functions for cleaner event handling
309
+ function initializeTestDataStore() {
310
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
311
+ global.testomatioDataStore.steps = [];
312
+ }
313
+
314
+ function buildArtifactFiles(artifacts) {
315
+ const files = [];
316
+ if (artifacts.screenshot) {
317
+ files.push({ path: artifacts.screenshot, type: 'image/png' });
318
+ }
319
+ return files;
320
+ }
321
+
322
+ function processArtifactsForUpload(artifacts, uid, title, videos, traces) {
323
+ for (const aid in artifacts) {
324
+ if (aid.startsWith('video')) {
325
+ videos.push({ rid: uid, title, path: artifacts[aid], type: 'video/webm' });
326
+ }
327
+ if (aid.startsWith('trace')) {
328
+ traces.push({ rid: uid, title, path: artifacts[aid], type: 'application/zip' });
329
+ }
330
+ }
331
+ }
332
+
333
+ function processMetaStepsForDisplay(step) {
334
+ const metaSteps = [];
335
+ let processingStep = step;
336
+
337
+ while (processingStep.metaStep) {
338
+ metaSteps.unshift(processingStep.metaStep);
339
+ processingStep = processingStep.metaStep;
340
+ }
341
+ }
342
+
343
+ function captureHookStep(step, currentHook, hookSteps) {
344
+ if (!currentHook) return;
345
+
346
+ const startTime = step.startTime;
347
+ const endTime = step.endTime;
348
+
349
+ const hookStepsArray = hookSteps.get(currentHook) || [];
350
+ hookStepsArray.push({
351
+ name: step.name,
352
+ actor: step.actor,
353
+ args: step.args,
354
+ status: step.status,
355
+ startTime,
356
+ endTime,
357
+ helperMethod: step.helperMethod,
358
+ });
359
+ hookSteps.set(currentHook, hookStepsArray);
360
+ }
361
+
362
+ // TODO: think about moving to some common utils
363
+ function getTestLogs(test) {
364
+ // Contexts for each log section
365
+ const suiteTitle = test.parent.fullTitle();
366
+ const testTitle = test.fullTitle();
367
+ const beforeSuiteLogsArr = services.logger.getLogs(`BeforeSuite ${suiteTitle}`);
368
+ const beforeLogsArr = services.logger.getLogs(`Before ${testTitle}`);
369
+ const testLogsArr = services.logger.getLogs(testTitle);
370
+ const afterLogsArr = services.logger.getLogs(`After ${testTitle}`);
371
+ const afterSuiteLogsArr = services.logger.getLogs(`AfterSuite ${suiteTitle}`);
372
+
373
+ const beforeSuiteLogs = beforeSuiteLogsArr ? beforeSuiteLogsArr.join('\n').trim() : '';
374
+ const beforeLogs = beforeLogsArr ? beforeLogsArr.join('\n').trim() : '';
375
+ const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
376
+ const afterLogs = afterLogsArr ? afterLogsArr.join('\n').trim() : '';
377
+ const afterSuiteLogs = afterSuiteLogsArr ? afterSuiteLogsArr.join('\n').trim() : '';
378
+
379
+ let logs = '';
380
+ if (beforeSuiteLogs) {
381
+ logs += `${pc.bold('--- BeforeSuite ---')}\n${beforeSuiteLogs}`;
382
+ }
383
+ if (beforeLogs) {
384
+ logs += `\n${pc.bold('--- Before ---')}\n${beforeLogs}`;
385
+ }
386
+ if (testLogs) {
387
+ logs += `\n${pc.bold('--- Test ---')}\n${testLogs}`;
388
+ }
389
+ if (afterLogs) {
390
+ logs += `\n${pc.bold('--- After ---')}\n${afterLogs}`;
391
+ }
392
+ if (afterSuiteLogs) {
393
+ logs += `\n${pc.bold('--- AfterSuite ---')}\n${afterSuiteLogs}`;
394
+ }
395
+ return logs;
396
+ }
397
+
398
+ // Build step hierarchy using CodeceptJS built-in methods
399
+ function buildUnifiedStepHierarchy(steps, hookSteps, screenshotOnFailPath = null) {
400
+ const hierarchy = [];
401
+
402
+ // Add pre-test hooks
403
+ addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.PRE_TEST);
404
+
405
+ // Process test steps if they exist
406
+ if (steps && steps.length > 0) {
407
+ processTestSteps(steps, hierarchy, screenshotOnFailPath);
408
+ }
409
+
410
+ // Add post-test hooks
411
+ addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.POST_TEST);
412
+
413
+ return hierarchy;
414
+ }
415
+
416
+ function addHooksToHierarchy(hierarchy, hookSteps, hookNames) {
417
+ for (const hookName of hookNames) {
418
+ if (hookSteps.has(hookName)) {
419
+ const hookSection = createHookSection(hookName, hookSteps.get(hookName));
420
+ if (hookSection) hierarchy.push(hookSection);
421
+ }
422
+ }
423
+ }
424
+
425
+ function processTestSteps(steps, hierarchy, screenshotOnFailPath = null) {
426
+ const sectionMap = new Map();
427
+ let screenshotAttached = false;
428
+
429
+ for (const step of steps) {
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);
437
+ if (!formattedStep) continue;
438
+
439
+ if (step.metaStep) {
440
+ // Step belongs to a section (meta step)
441
+ const sectionKey = step.metaStep;
442
+ let sectionStep = sectionMap.get(sectionKey);
443
+
444
+ if (!sectionStep) {
445
+ sectionStep = createSectionStep(step.metaStep);
446
+ sectionMap.set(sectionKey, sectionStep);
447
+ hierarchy.push(sectionStep);
448
+ }
449
+
450
+ sectionStep.steps.push(formattedStep);
451
+ sectionStep.duration += formattedStep.duration || 0;
452
+ } else {
453
+ // Regular step
454
+ hierarchy.push(formattedStep);
455
+ }
456
+ }
457
+ }
458
+
459
+ function createSectionStep(metaStep) {
460
+ return {
461
+ category: 'user',
462
+ title: metaStep.toString(), // Use built-in toString method
463
+ duration: metaStep.duration || 0, // Use built-in duration
464
+ steps: [],
465
+ };
466
+ }
467
+
468
+ function createHookSection(hookName, steps) {
469
+ if (!steps || steps.length === 0) return null;
470
+
471
+ const hookSection = {
472
+ category: 'hook',
473
+ title: formatHookName(hookName),
474
+ duration: 0,
475
+ steps: [],
476
+ };
477
+
478
+ for (const step of steps) {
479
+ const formattedStep = formatHookStep(step);
480
+ if (formattedStep) {
481
+ hookSection.steps.push(formattedStep);
482
+ hookSection.duration += formattedStep.duration || 0;
483
+ }
484
+ }
485
+
486
+ return hookSection.steps.length > 0 ? hookSection : null;
487
+ }
488
+
489
+ function formatHookName(hookName) {
490
+ return hookName.replace(/Hook$/, '');
491
+ }
492
+
493
+ function getCodeceptStepCategory(origin = 'test') {
494
+ if (origin === 'hook') return 'hook';
495
+ return 'user';
496
+ }
497
+
498
+ // Format CodeceptJS step using its built-in methods
499
+ function formatCodeceptStep(step, screenshotOnFailPath = null) {
500
+ if (!step) return null;
501
+
502
+ const category = getCodeceptStepCategory('test');
503
+ const title = truncate(String(step));
504
+ const duration = step.duration || 0;
505
+
506
+ const formattedStep = formatStep({
507
+ category,
508
+ title,
509
+ duration,
510
+ });
511
+
512
+ // Add status
513
+ addStatusToStep(formattedStep, step.status, step.err);
514
+
515
+ // Add error if step failed
516
+ if (step.status === 'failed' && step.err) {
517
+ formattedStep.error = {
518
+ message: truncate(String(step.err.message || 'Step failed'), 250),
519
+ stack: truncate(String(step.err.stack || ''), 250),
520
+ };
521
+ }
522
+
523
+ // Add artifacts
524
+ if (step.artifacts && SCREENSHOTS_ON_STEPS) {
525
+ addArtifactsToStep(formattedStep, step.artifacts);
526
+ }
527
+
528
+ // Add screenshot from screenshotOnFail plugin
529
+ if (screenshotOnFailPath && SCREENSHOTS_ON_STEPS) {
530
+ addArtifactPathToStep(formattedStep, screenshotOnFailPath);
531
+ }
532
+
533
+ // Add log if present
534
+ if (step.log) {
535
+ formattedStep.log = truncate(String(step.log), 250);
536
+ }
537
+
538
+ return formattedStep;
539
+ }
540
+
541
+ function formatHookStep(step) {
542
+ if (!step) return null;
543
+
544
+ // For hook steps, construct title from available properties
545
+ let title = step.name;
546
+ if (step.actor && step.name) {
547
+ title = `${step.actor} ${step.name}`;
548
+ if (step.args && step.args.length > 0) {
549
+ const argsStr = step.args.map(arg => truncate(JSON.stringify(arg), 250)).join(', ');
550
+ title += ` ${argsStr}`;
551
+ }
552
+ }
553
+ title = truncate(title);
554
+
555
+ const formattedStep = formatStep({
556
+ category: getCodeceptStepCategory('hook'),
557
+ title,
558
+ duration: step.duration || 0,
559
+ });
560
+
561
+ addStatusToStep(formattedStep, step.status, step.err);
562
+
563
+ if (step.status === 'failed' && step.err) {
564
+ formattedStep.error = {
565
+ message: truncate(String(step.err.message || 'Hook failed'), 250),
566
+ stack: truncate(String(step.err.stack || ''), 250),
567
+ };
568
+ }
569
+
570
+ // Add artifacts
571
+ if (step.artifacts && SCREENSHOTS_ON_STEPS) {
572
+ addArtifactsToStep(formattedStep, step.artifacts);
573
+ }
574
+
575
+ // Add log if present
576
+ if (step.log) {
577
+ formattedStep.log = truncate(String(step.log), 250);
578
+ }
579
+
580
+ return formattedStep;
581
+ }
582
+
583
+ export { CodeceptReporter };
584
+ export default CodeceptReporter;
585
+
586
+ function buildCodeceptClientConfig(config = {}) {
587
+ const outputDir = resolveCodeceptOutputDir(config);
588
+ const reportDir = resolveCodeceptReportDir(config, outputDir);
589
+
590
+ return {
591
+ ...config,
592
+ apiKey: config.apiKey,
593
+ framework: 'codeceptjs',
594
+ outputDir,
595
+ reportDir,
596
+ html: config.html,
597
+ markdown: config.markdown,
598
+ csv: config.csv,
599
+ };
600
+ }
601
+
602
+ function resolveCodeceptOutputDir(config = {}) {
603
+ const codeceptStore = /** @type {{ outputDir?: string }} */ (codeceptjs.store || {});
604
+ const candidates = [
605
+ config.outputDir,
606
+ config.output,
607
+ codeceptStore.outputDir,
608
+ codecept?.config?.get?.()?.output,
609
+ codecept?.config?.output,
610
+ ];
611
+
612
+ const outputDir = candidates.find(value => typeof value === 'string' && value.trim());
613
+ return outputDir || 'output';
614
+ }
615
+
616
+ function resolveCodeceptReportDir(config = {}, outputDir = 'output') {
617
+ if (typeof config.reportDir === 'string' && config.reportDir.trim()) {
618
+ return config.reportDir;
619
+ }
620
+
621
+ if (path.isAbsolute(outputDir)) {
622
+ return path.join(outputDir, 'report');
623
+ }
624
+
625
+ return path.join(outputDir, 'report');
626
+ }