@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.
- package/lib/adapter/codecept.js +287 -203
- package/package.json +10 -5
- package/src/adapter/codecept.js +319 -215
package/lib/adapter/codecept.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
// fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
55
82
|
// recorder.add('Creating new run', () => );
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
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.
|
|
74
|
-
|
|
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.
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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(
|
|
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:
|
|
151
|
+
time: test.duration,
|
|
195
152
|
files,
|
|
196
|
-
steps:
|
|
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
|
-
|
|
159
|
+
processArtifactsForUpload(artifacts, uid, title, videos, traces);
|
|
225
160
|
});
|
|
226
161
|
event.dispatcher.on(event.step.started, step => {
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
317
|
-
const
|
|
318
|
-
const
|
|
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 (
|
|
322
|
-
logs += `${picocolors_1.default.bold('
|
|
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('
|
|
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.
|
|
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
|
|
58
|
-
"test:
|
|
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 &&
|
|
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": {
|
package/src/adapter/codecept.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
let error;
|
|
21
|
-
let stepShift = 0;
|
|
20
|
+
const [, MAJOR_VERSION, MINOR_VERSION] = codecept.version().match(/(\d+)\.(\d+)/).map(Number);
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
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.
|
|
92
|
-
|
|
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.
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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(
|
|
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:
|
|
176
|
+
time: test.duration,
|
|
224
177
|
files,
|
|
225
|
-
steps:
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
const
|
|
363
|
-
const
|
|
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 (
|
|
368
|
-
logs += `${pc.bold('
|
|
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('
|
|
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;
|