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.
- package/README.md +3 -3
- package/bin/cli.js +6 -26
- package/package.json +39 -4
- package/src/adapter/codecept.js +626 -0
- package/src/adapter/cucumber/current.js +230 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +108 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +211 -0
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +343 -0
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +455 -0
- package/src/adapter/webdriver.js +201 -0
- package/src/bin/cli.js +507 -0
- package/src/bin/reportXml.js +79 -0
- package/src/bin/startTest.js +54 -0
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +524 -0
- package/src/config.js +30 -0
- package/src/constants.js +72 -0
- package/src/data-storage.js +204 -0
- package/src/helpers.js +1 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +70 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +285 -0
- package/src/pipe/coverage.js +500 -0
- package/src/pipe/csv.js +161 -0
- package/src/pipe/debug.js +143 -0
- package/src/pipe/github.js +256 -0
- package/src/pipe/gitlab.js +258 -0
- package/src/pipe/html.js +1153 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/markdown.js +753 -0
- package/src/pipe/testomatio.js +707 -0
- package/src/replay.js +274 -0
- package/src/reporter-functions.js +155 -0
- package/src/reporter.js +42 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +15 -0
- package/src/services/key-values.js +59 -0
- package/src/services/links.js +69 -0
- package/src/services/logger.js +320 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3726 -0
- package/src/uploader.js +382 -0
- package/src/utils/constants.js +12 -0
- package/src/utils/debug.js +20 -0
- package/src/utils/log-formatter.js +118 -0
- package/src/utils/log.js +88 -0
- package/src/utils/pipe_utils.js +193 -0
- package/src/utils/utils.js +732 -0
- package/src/xmlReader.js +834 -0
- package/types/types.d.ts +425 -0
- 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
|
+
}
|