@testomatio/reporter 1.1.0-beta-3 → 1.1.0-beta.codeceptjs-before-suite-logs
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/Changelog.md +4 -0
- package/lib/adapter/codecept.js +31 -6
- package/lib/adapter/cypress-plugin/index.js +11 -4
- package/lib/bin/startTest.js +6 -6
- package/lib/config.js +24 -0
- package/lib/constants.js +7 -0
- package/lib/fileUploader.js +121 -44
- package/lib/pipe/gitlab.js +1 -1
- package/lib/pipe/html.js +316 -0
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.js +2 -3
- package/lib/storages/logger.js +2 -2
- package/lib/template/template-draft.hbs +249 -0
- package/lib/template/testomatio.hbs +750 -0
- package/lib/xmlReader.js +1 -2
- package/package.json +6 -2
package/Changelog.md
CHANGED
package/lib/adapter/codecept.js
CHANGED
|
@@ -78,8 +78,35 @@ function CodeceptReporter(config) {
|
|
|
78
78
|
global.testomatioDataStore.steps = [];
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
let hookSteps = [];
|
|
82
|
+
let suiteHookRunning = false;
|
|
83
|
+
|
|
84
|
+
event.dispatcher.on(event.suite.before, (suite) => {
|
|
85
|
+
suiteHookRunning = true;
|
|
86
|
+
hookSteps = [];
|
|
87
|
+
global.testomatioDataStore.steps = [];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
event.dispatcher.on(event.test.before, () => {
|
|
91
|
+
suiteHookRunning = false;
|
|
92
|
+
global.testomatioDataStore.steps = []
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
event.dispatcher.on(event.hook.started, (suite) => {
|
|
96
|
+
// global.testomatioDataStore.steps = [];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
event.dispatcher.on(event.hook.passed, (suite) => {
|
|
100
|
+
if (suiteHookRunning) hookSteps.push(...global.testomatioDataStore.steps);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
event.dispatcher.on(event.hook.failed, (suite) => {
|
|
104
|
+
if (suiteHookRunning) hookSteps.push(...global.testomatioDataStore.steps);
|
|
105
|
+
});
|
|
106
|
+
|
|
81
107
|
event.dispatcher.on(event.test.started, test => {
|
|
82
108
|
testTimeMap[test.id] = Date.now();
|
|
109
|
+
|
|
83
110
|
if (global.testomatioDataStore) global.testomatioDataStore.currentlyRunningTestId = getIdFromTestTitle(test.title);
|
|
84
111
|
});
|
|
85
112
|
|
|
@@ -89,7 +116,7 @@ function CodeceptReporter(config) {
|
|
|
89
116
|
await Promise.all(reportTestPromises);
|
|
90
117
|
|
|
91
118
|
if (upload.isArtifactsEnabled()) {
|
|
92
|
-
uploadAttachments(client, videos, '🎞️
|
|
119
|
+
uploadAttachments(client, videos, '🎞️ Uploading', 'video');
|
|
93
120
|
uploadAttachments(client, traces, '📁 Uploading', 'trace');
|
|
94
121
|
}
|
|
95
122
|
|
|
@@ -109,7 +136,7 @@ function CodeceptReporter(config) {
|
|
|
109
136
|
suite_title: test.parent && test.parent.title,
|
|
110
137
|
message: testObj.message,
|
|
111
138
|
time: getDuration(test),
|
|
112
|
-
steps: global.testomatioDataStore.steps.join('\n') || null,
|
|
139
|
+
steps: [...hookSteps, ...global.testomatioDataStore.steps].join('\n') || null,
|
|
113
140
|
test_id: testId,
|
|
114
141
|
});
|
|
115
142
|
// output.stop();
|
|
@@ -133,6 +160,7 @@ function CodeceptReporter(config) {
|
|
|
133
160
|
...stripExampleFromTitle(title),
|
|
134
161
|
suite_title: suite.title,
|
|
135
162
|
test_id: testId,
|
|
163
|
+
steps: hookSteps.join('\n') || null,
|
|
136
164
|
error,
|
|
137
165
|
time: 0,
|
|
138
166
|
});
|
|
@@ -147,9 +175,6 @@ function CodeceptReporter(config) {
|
|
|
147
175
|
failedTests.push(id || title);
|
|
148
176
|
let testId = parseTest(tags);
|
|
149
177
|
const testObj = getTestAndMessage(title);
|
|
150
|
-
if (error && error.stack && test.steps && test.steps.length) {
|
|
151
|
-
error.stack = test.steps[test.steps.length - 1].line();
|
|
152
|
-
}
|
|
153
178
|
|
|
154
179
|
const files = [];
|
|
155
180
|
if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
|
|
@@ -164,7 +189,7 @@ function CodeceptReporter(config) {
|
|
|
164
189
|
message: testObj.message,
|
|
165
190
|
time: getDuration(test),
|
|
166
191
|
files,
|
|
167
|
-
steps: global.testomatioDataStore
|
|
192
|
+
steps: [...hookSteps, ...global.testomatioDataStore.steps].join('\n') || null,
|
|
168
193
|
})
|
|
169
194
|
.then(pipes => {
|
|
170
195
|
testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
|
|
@@ -10,6 +10,7 @@ const testomatioReporter = on => {
|
|
|
10
10
|
const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
|
|
11
11
|
|
|
12
12
|
on('before:run', async (run) => {
|
|
13
|
+
// TODO: looks like client.env does not exist
|
|
13
14
|
if (!client.env) {
|
|
14
15
|
client.env = `${run.browser.displayName},${run.system.osName}`
|
|
15
16
|
}
|
|
@@ -48,10 +49,16 @@ const testomatioReporter = on => {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const screenshots = results.screenshots
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const screenshots = Array.isArray(results.screenshots)
|
|
53
|
+
? results.screenshots
|
|
54
|
+
.filter(
|
|
55
|
+
screenshot =>
|
|
56
|
+
screenshot?.path &&
|
|
57
|
+
screenshot?.path.includes(title) &&
|
|
58
|
+
screenshot?.takenAt
|
|
59
|
+
)
|
|
60
|
+
.map(screenshot => screenshot.path)
|
|
61
|
+
: [];
|
|
55
62
|
|
|
56
63
|
const files = [...videos, ...screenshots];
|
|
57
64
|
|
package/lib/bin/startTest.js
CHANGED
|
@@ -53,6 +53,12 @@ program
|
|
|
53
53
|
|
|
54
54
|
let exitCode = 0;
|
|
55
55
|
|
|
56
|
+
if (!command.split) {
|
|
57
|
+
process.exitCode = 255;
|
|
58
|
+
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
57
63
|
|
|
58
64
|
if(filter) {
|
|
@@ -74,12 +80,6 @@ program
|
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
if (!command.split) {
|
|
78
|
-
process.exitCode = 255;
|
|
79
|
-
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
83
|
const testCmds = command.split(' ');
|
|
84
84
|
console.log(APP_PREFIX, `🚀 Running`, chalk.green(command));
|
|
85
85
|
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// This file is used to read environment variables from .env file and process.env
|
|
2
|
+
|
|
3
|
+
// ! uncommenting next line leads ro reading vars from .env file
|
|
4
|
+
// require('dotenv').config();
|
|
5
|
+
const debug = require('debug')('@testomatio/reporter:config');
|
|
6
|
+
|
|
7
|
+
/* for possibility to use multiple env files (reading different paths)
|
|
8
|
+
const dotenv = require('dotenv');
|
|
9
|
+
const envFileVars = dotenv.config({ path: '.env' }).parsed; */
|
|
10
|
+
|
|
11
|
+
// select only TESTOMATIO related variables (only to print them in debug)
|
|
12
|
+
const testomatioEnvVars =
|
|
13
|
+
Object.keys(process.env)
|
|
14
|
+
.filter(key => key.startsWith('TESTOMATIO') || key.startsWith('S3_'))
|
|
15
|
+
.reduce((obj, key) => {
|
|
16
|
+
obj[key] = process.env[key];
|
|
17
|
+
return obj;
|
|
18
|
+
}, {}) || {};
|
|
19
|
+
debug('TESTOMATIO variables:', testomatioEnvVars);
|
|
20
|
+
|
|
21
|
+
// includes variables from .env file and process.env
|
|
22
|
+
const config = process.env;
|
|
23
|
+
|
|
24
|
+
module.exports = config;
|
package/lib/constants.js
CHANGED
|
@@ -20,10 +20,17 @@ const STATUS = {
|
|
|
20
20
|
SKIPPED: 'skipped',
|
|
21
21
|
FINISHED: 'finished',
|
|
22
22
|
};
|
|
23
|
+
// html pipe var
|
|
24
|
+
const HTML_REPORT = {
|
|
25
|
+
FOLDER: "html-report",
|
|
26
|
+
REPORT_DEFAULT_NAME: "testomatio-report.html",
|
|
27
|
+
TEMPLATE_NAME: 'testomatio.hbs'
|
|
28
|
+
};
|
|
23
29
|
|
|
24
30
|
module.exports = {
|
|
25
31
|
APP_PREFIX,
|
|
26
32
|
TESTOMAT_TMP_STORAGE_DIR,
|
|
27
33
|
CSV_HEADERS,
|
|
28
34
|
STATUS,
|
|
35
|
+
HTML_REPORT
|
|
29
36
|
}
|
package/lib/fileUploader.js
CHANGED
|
@@ -3,10 +3,16 @@ const { S3 } = require('@aws-sdk/client-s3');
|
|
|
3
3
|
const { Upload } = require('@aws-sdk/lib-storage');
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const util = require('util');
|
|
6
7
|
const path = require('path');
|
|
8
|
+
const promiseRetry = require('promise-retry');
|
|
9
|
+
|
|
10
|
+
const readFile = util.promisify(fs.readFile);
|
|
11
|
+
const stat = util.promisify(fs.stat);
|
|
7
12
|
const chalk = require('chalk');
|
|
8
13
|
const { randomUUID } = require('crypto');
|
|
9
14
|
const memoize = require('lodash.memoize');
|
|
15
|
+
|
|
10
16
|
const { APP_PREFIX } = require('./constants');
|
|
11
17
|
|
|
12
18
|
const keys = [
|
|
@@ -93,51 +99,64 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
93
99
|
Key = filePath.name;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
if (!fs.existsSync(filePath)) {
|
|
97
|
-
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
102
|
const {
|
|
102
|
-
TESTOMATIO_PRIVATE_ARTIFACTS,
|
|
103
|
+
TESTOMATIO_PRIVATE_ARTIFACTS,
|
|
104
|
+
S3_BUCKET
|
|
103
105
|
} = getConfig();
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const fileData = fs.readFileSync(filePath);
|
|
107
|
+
try {
|
|
108
|
+
debug('S3 config', getMaskedConfig());
|
|
109
|
+
debug('Started upload', filePath, 'to ', S3_BUCKET);
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
// Verification that the file was actually created: 20 attempts of 0.5 second => 10sec
|
|
112
|
+
const isFileExist = await checkFileExists(filePath, 20, 500);
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
if (!isFileExist) {
|
|
115
|
+
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Body: fileData,
|
|
119
|
-
ContentType,
|
|
120
|
-
ACL,
|
|
121
|
-
};
|
|
119
|
+
debug('File: ', filePath, ' exists');
|
|
120
|
+
|
|
121
|
+
const fileData = await readFile(filePath);
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
|
|
124
|
+
|
|
125
|
+
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
126
|
+
|
|
127
|
+
if (!S3_BUCKET || !fileData) {
|
|
128
|
+
console.log(
|
|
129
|
+
APP_PREFIX,
|
|
130
|
+
chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const s3 = new S3(_getS3Config());
|
|
135
|
+
|
|
136
|
+
const params = {
|
|
137
|
+
Bucket: S3_BUCKET,
|
|
138
|
+
Key,
|
|
139
|
+
Body: fileData,
|
|
140
|
+
ContentType,
|
|
141
|
+
ACL,
|
|
142
|
+
};
|
|
143
|
+
|
|
124
144
|
const out = new Upload({
|
|
125
145
|
client: s3,
|
|
126
146
|
params
|
|
127
147
|
});
|
|
128
148
|
|
|
129
|
-
await out
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
console.log(e);
|
|
135
|
-
console.log(APP_PREFIX, chalk.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
|
|
136
|
-
|
|
149
|
+
return await getS3LocationLink(out);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
debug('S3 file uploading error: ', e);
|
|
153
|
+
|
|
137
154
|
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
155
|
+
|
|
138
156
|
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
139
157
|
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
140
|
-
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
141
160
|
console.log(
|
|
142
161
|
APP_PREFIX,
|
|
143
162
|
`To enable ${chalk.bold('PUBLIC')} uploads remove TESTOMATIO_PRIVATE_ARTIFACTS env variable`,
|
|
@@ -148,7 +167,6 @@ const fileData = fs.readFileSync(filePath);
|
|
|
148
167
|
};
|
|
149
168
|
|
|
150
169
|
const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
151
|
-
|
|
152
170
|
const {
|
|
153
171
|
S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
154
172
|
} = getConfig();
|
|
@@ -158,6 +176,18 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
|
158
176
|
const fileExtension = _getFileExtBase64(buffer.toString('base64'));
|
|
159
177
|
const Key = `${runId}/${fileName}${fileExtension}`;
|
|
160
178
|
|
|
179
|
+
if (!S3_BUCKET || !buffer) {
|
|
180
|
+
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
181
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
182
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
183
|
+
region: S3_REGION,
|
|
184
|
+
bucket: S3_BUCKET,
|
|
185
|
+
acl: ACL,
|
|
186
|
+
endpoint: S3_ENDPOINT,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
161
191
|
const s3 = new S3(_getS3Config());
|
|
162
192
|
|
|
163
193
|
try {
|
|
@@ -171,20 +201,14 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
|
171
201
|
ACL,
|
|
172
202
|
}
|
|
173
203
|
});
|
|
174
|
-
await out.done();
|
|
175
204
|
|
|
176
|
-
return out
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
181
|
-
region: S3_REGION,
|
|
182
|
-
bucket: S3_BUCKET,
|
|
183
|
-
acl: ACL,
|
|
184
|
-
endpoint: S3_ENDPOINT,
|
|
185
|
-
});
|
|
205
|
+
return await getS3LocationLink(out);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
debug('S3 buffer uploading error: ', e);
|
|
186
209
|
|
|
187
210
|
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
211
|
+
|
|
188
212
|
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
189
213
|
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
190
214
|
} else {
|
|
@@ -203,7 +227,9 @@ const uploadFileByPath = async (filePath, runId) => {
|
|
|
203
227
|
return uploadUsingS3(filePath, runId);
|
|
204
228
|
}
|
|
205
229
|
} catch (e) {
|
|
206
|
-
|
|
230
|
+
debug(e);
|
|
231
|
+
|
|
232
|
+
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
207
233
|
}
|
|
208
234
|
};
|
|
209
235
|
|
|
@@ -213,10 +239,61 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
|
213
239
|
return uploadUsingS3AsBuffer(buffer, fileName, runId);
|
|
214
240
|
}
|
|
215
241
|
} catch (e) {
|
|
216
|
-
|
|
242
|
+
debug(e);
|
|
243
|
+
|
|
244
|
+
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
217
245
|
}
|
|
218
246
|
};
|
|
219
247
|
|
|
248
|
+
const checkFileExists = async (filePath, attempts = 5, intervalMs = 500) => {
|
|
249
|
+
const checkFile = async () => {
|
|
250
|
+
const fileStats = await stat(filePath);
|
|
251
|
+
if (fileStats.isFile()) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
throw new Error('File not found');
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await promiseRetry(
|
|
260
|
+
{
|
|
261
|
+
retries: attempts,
|
|
262
|
+
minTimeout: intervalMs
|
|
263
|
+
},
|
|
264
|
+
checkFile
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return true;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(
|
|
270
|
+
chalk.yellow(`File ${filePath} was not found or did not have time to be generated...`)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const getS3LocationLink = async (out) => {
|
|
278
|
+
const response = await out.done();
|
|
279
|
+
|
|
280
|
+
let s3Location = response?.Location;
|
|
281
|
+
|
|
282
|
+
debug('Uploaded response.Location', s3Location);
|
|
283
|
+
|
|
284
|
+
if (!s3Location) {
|
|
285
|
+
// TODO: out: a fallback case - remove after deeper testing
|
|
286
|
+
s3Location = out?.singleUploadResult?.Location;
|
|
287
|
+
debug('Uploaded singleUploadResult.Location', s3Location);
|
|
288
|
+
|
|
289
|
+
if (!s3Location) {
|
|
290
|
+
throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return s3Location;
|
|
295
|
+
};
|
|
296
|
+
|
|
220
297
|
module.exports = {
|
|
221
298
|
uploadFileByPath: memoize(uploadFileByPath),
|
|
222
299
|
uploadFileAsBuffer: memoize(uploadFileAsBuffer),
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -23,7 +23,7 @@ class GitLabPipe {
|
|
|
23
23
|
this.store = store;
|
|
24
24
|
this.tests = [];
|
|
25
25
|
// GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
|
|
26
|
-
this.token = params.GITLAB_PAT || this.ENV.GITLAB_PAT;
|
|
26
|
+
this.token = params.GITLAB_PAT || process.env.GITLAB_PAT || this.ENV.GITLAB_PAT;
|
|
27
27
|
this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
|
|
28
28
|
|
|
29
29
|
debug(
|