artes 1.5.2 → 1.5.4
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/cucumber.config.js +1 -1
- package/executer.js +5 -237
- package/package.json +1 -1
- package/src/helper/controller/findDuplicateTestNames.js +54 -0
- package/src/helper/controller/getExecutor.js +104 -0
- package/src/helper/controller/reportCustomizer.js +205 -10
- package/src/helper/controller/testCoverageCalculator.js +86 -0
- package/src/hooks/hooks.js +28 -29
- /package/{status-formatter.js → src/helper/controller/status-formatter.js} +0 -0
package/cucumber.config.js
CHANGED
|
@@ -14,7 +14,7 @@ try {
|
|
|
14
14
|
console.log("Proceeding with default config.");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const defaultFormats = ["rerun:@rerun.txt", "progress-bar", './status-formatter.js:null'];
|
|
17
|
+
const defaultFormats = ["rerun:@rerun.txt", "progress-bar", './src/helper/controller/status-formatter.js:null'];
|
|
18
18
|
|
|
19
19
|
const userFormatsFromEnv = process.env.REPORT_FORMAT
|
|
20
20
|
? JSON.parse(process.env.REPORT_FORMAT)
|
package/executer.js
CHANGED
|
@@ -10,6 +10,10 @@ const {
|
|
|
10
10
|
const { logPomWarnings } = require("./src/helper/controller/pomCollector");
|
|
11
11
|
const fs = require("fs");
|
|
12
12
|
const path = require("path");
|
|
13
|
+
const { testCoverageCalculator } = require("./src/helper/controller/testCoverageCalculator");
|
|
14
|
+
const { getExecutor } = require("./src/helper/controller/getExecutor");
|
|
15
|
+
const { findDuplicateTestNames } = require("./src/helper/controller/findDuplicateTestNames");
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
const artesConfigPath = path.resolve(process.cwd(), "artes.config.js");
|
|
15
19
|
|
|
@@ -168,242 +172,6 @@ flags.timeout ? (process.env.TIMEOUT = timeout) : "";
|
|
|
168
172
|
flags.slowMo ? (process.env.SLOWMO = slowMo) : "";
|
|
169
173
|
|
|
170
174
|
|
|
171
|
-
function findDuplicateTestNames() {
|
|
172
|
-
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
173
|
-
|
|
174
|
-
if (!fs.existsSync(testStatusFile)) {
|
|
175
|
-
console.error('test-status.txt not found');
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
180
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
181
|
-
|
|
182
|
-
const testNameToFiles = {};
|
|
183
|
-
|
|
184
|
-
lines.forEach(line => {
|
|
185
|
-
const parts = line.split(' | ');
|
|
186
|
-
if (parts.length < 5) return;
|
|
187
|
-
|
|
188
|
-
const testName = parts[2].trim();
|
|
189
|
-
const filePath = parts[4].trim();
|
|
190
|
-
|
|
191
|
-
if (!testNameToFiles[testName]) {
|
|
192
|
-
testNameToFiles[testName] = new Set();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
testNameToFiles[testName].add(filePath);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const duplicates = {};
|
|
199
|
-
|
|
200
|
-
Object.entries(testNameToFiles).forEach(([testName, files]) => {
|
|
201
|
-
if (files.size > 1) {
|
|
202
|
-
duplicates[testName] = Array.from(files);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (Object.keys(duplicates).length > 0) {
|
|
207
|
-
console.warn('\n\x1b[33m[WARNING] Duplicate scenarios names found: This will effect your reporting');
|
|
208
|
-
Object.entries(duplicates).forEach(([testName, files]) => {
|
|
209
|
-
console.log(`\x1b[33m"${testName}" exists in:`);
|
|
210
|
-
files.forEach(file => {
|
|
211
|
-
console.log(` - ${file}`);
|
|
212
|
-
});
|
|
213
|
-
console.log('');
|
|
214
|
-
});
|
|
215
|
-
console.log("\x1b[0m");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return duplicates;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
function testCoverageCalculation() {
|
|
223
|
-
|
|
224
|
-
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
225
|
-
|
|
226
|
-
if (!fs.existsSync(testStatusFile)) {
|
|
227
|
-
console.error('test-status.txt not found');
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
232
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
233
|
-
|
|
234
|
-
const map = {};
|
|
235
|
-
const retriedTests = [];
|
|
236
|
-
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
237
|
-
|
|
238
|
-
lines.forEach(line => {
|
|
239
|
-
const parts = line.split(' | ');
|
|
240
|
-
if (parts.length < 5) return;
|
|
241
|
-
|
|
242
|
-
const timestamp = parts[0].trim();
|
|
243
|
-
const status = parts[1].trim();
|
|
244
|
-
const scenario = parts[2].trim();
|
|
245
|
-
const id = parts[3].trim();
|
|
246
|
-
const uri = parts[4].trim();
|
|
247
|
-
|
|
248
|
-
if (!uuidRegex.test(id)) return;
|
|
249
|
-
|
|
250
|
-
if (!map[id]) {
|
|
251
|
-
map[id] = {
|
|
252
|
-
count: 1,
|
|
253
|
-
latest: { status, scenario, timestamp, uri }
|
|
254
|
-
};
|
|
255
|
-
} else {
|
|
256
|
-
map[id].count++;
|
|
257
|
-
if (timestamp > map[id].latest.timestamp) {
|
|
258
|
-
map[id].latest = { status, scenario, timestamp, uri };
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
let total = 0;
|
|
264
|
-
let notPassed = 0;
|
|
265
|
-
|
|
266
|
-
Object.entries(map).forEach(([id, data]) => {
|
|
267
|
-
total++;
|
|
268
|
-
|
|
269
|
-
if (data.count > 1) {
|
|
270
|
-
retriedTests.push({
|
|
271
|
-
scenario: data.latest.scenario,
|
|
272
|
-
id,
|
|
273
|
-
count: data.count
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (data.latest.status !== 'PASSED') {
|
|
278
|
-
notPassed++;
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
if (retriedTests.length > 0) {
|
|
283
|
-
console.warn('\n\x1b[33mRetried test cases:');
|
|
284
|
-
retriedTests.forEach(t => {
|
|
285
|
-
console.warn(`- "${t.scenario}" ran ${t.count} times`);
|
|
286
|
-
});
|
|
287
|
-
console.log("\x1b[0m");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
percentage: (total - notPassed) / total * 100,
|
|
292
|
-
totalTests: total,
|
|
293
|
-
notPassed,
|
|
294
|
-
passed: total - notPassed,
|
|
295
|
-
latestStatuses: Object.fromEntries(
|
|
296
|
-
Object.entries(map).map(([id, data]) => [
|
|
297
|
-
id,
|
|
298
|
-
data.latest.status
|
|
299
|
-
])
|
|
300
|
-
)
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function getExecutor() {
|
|
305
|
-
|
|
306
|
-
if (process.env.GITHUB_RUN_ID) {
|
|
307
|
-
return {
|
|
308
|
-
name: "GitHub Actions",
|
|
309
|
-
type: "github",
|
|
310
|
-
buildName: `Workflow #${process.env.GITHUB_RUN_NUMBER}`,
|
|
311
|
-
buildOrder: Number(process.env.GITHUB_RUN_NUMBER),
|
|
312
|
-
buildUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
} else if (process.env.JENKINS_HOME) {
|
|
317
|
-
return {
|
|
318
|
-
name: "Jenkins",
|
|
319
|
-
type: "jenkins",
|
|
320
|
-
buildName: process.env.JOB_NAME || "Manual Run",
|
|
321
|
-
buildOrder: Number(process.env.BUILD_NUMBER) || 1,
|
|
322
|
-
buildUrl: process.env.BUILD_URL || ""
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} else if (process.env.CI_PIPELINE_ID) {
|
|
327
|
-
return {
|
|
328
|
-
name: "GitLab CI",
|
|
329
|
-
type: "gitlab",
|
|
330
|
-
buildName: `Pipeline #${process.env.CI_PIPELINE_IID}`,
|
|
331
|
-
buildOrder: Number(process.env.CI_PIPELINE_IID) || 1,
|
|
332
|
-
buildUrl: process.env.CI_PIPELINE_URL || ""
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
} else if (process.env.BITBUCKET_BUILD_NUMBER) {
|
|
337
|
-
return {
|
|
338
|
-
name: "Bitbucket Pipelines",
|
|
339
|
-
type: "bitbucket",
|
|
340
|
-
buildName: `Build #${process.env.BITBUCKET_BUILD_NUMBER}`,
|
|
341
|
-
buildOrder: Number(process.env.BITBUCKET_BUILD_NUMBER),
|
|
342
|
-
buildUrl: process.env.BITBUCKET_BUILD_URL || ""
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} else if (process.env.CIRCLE_WORKFLOW_ID) {
|
|
347
|
-
return {
|
|
348
|
-
name: "CircleCI",
|
|
349
|
-
type: "circleci",
|
|
350
|
-
buildName: `Workflow #${process.env.CIRCLE_WORKFLOW_ID}`,
|
|
351
|
-
buildOrder: Number(process.env.CIRCLE_BUILD_NUM) || 1,
|
|
352
|
-
buildUrl: process.env.CIRCLE_BUILD_URL || ""
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} else if (process.env.BUILD_BUILDID) {
|
|
357
|
-
return {
|
|
358
|
-
name: "Azure Pipelines",
|
|
359
|
-
type: "azure",
|
|
360
|
-
buildName: `Build #${process.env.BUILD_BUILDID}`,
|
|
361
|
-
buildOrder: Number(process.env.BUILD_BUILDID) || 1,
|
|
362
|
-
buildUrl: process.env.BUILD_BUILDURI || ""
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
} else if (process.env.BUILD_NUMBER && process.env.TEAMCITY_VERSION) {
|
|
367
|
-
return {
|
|
368
|
-
name: "TeamCity",
|
|
369
|
-
type: "teamcity",
|
|
370
|
-
buildName: `Build #${process.env.BUILD_NUMBER}`,
|
|
371
|
-
buildOrder: Number(process.env.BUILD_NUMBER) || 1,
|
|
372
|
-
buildUrl: process.env.BUILD_URL || ""
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
} else if (process.env.TRAVIS_BUILD_NUMBER) {
|
|
377
|
-
return {
|
|
378
|
-
name: "Travis CI",
|
|
379
|
-
type: "travis",
|
|
380
|
-
buildName: `Build #${process.env.TRAVIS_BUILD_NUMBER}`,
|
|
381
|
-
buildOrder: Number(process.env.TRAVIS_BUILD_NUMBER) || 1,
|
|
382
|
-
buildUrl: process.env.TRAVIS_BUILD_WEB_URL || ""
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
} else if (process.env.bamboo_buildNumber) {
|
|
387
|
-
return {
|
|
388
|
-
name: "Bamboo",
|
|
389
|
-
type: "bamboo",
|
|
390
|
-
buildName: `Build #${process.env.bamboo_buildNumber}`,
|
|
391
|
-
buildOrder: Number(process.env.bamboo_buildNumber) || 1,
|
|
392
|
-
buildUrl: process.env.bamboo_resultsUrl || ""
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
} else {
|
|
397
|
-
return {
|
|
398
|
-
name: "Local Run",
|
|
399
|
-
type: "local",
|
|
400
|
-
buildName: "Manual Execution",
|
|
401
|
-
buildOrder: 1,
|
|
402
|
-
buildUrl: ""
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
175
|
|
|
408
176
|
function main() {
|
|
409
177
|
if (flags.help) return showHelp();
|
|
@@ -416,7 +184,7 @@ function main() {
|
|
|
416
184
|
|
|
417
185
|
findDuplicateTestNames();
|
|
418
186
|
|
|
419
|
-
const testCoverage =
|
|
187
|
+
const testCoverage = testCoverageCalculator()
|
|
420
188
|
|
|
421
189
|
const testPercentage = (process.env.PERCENTAGE ? Number(process.env.PERCENTAGE) : artesConfig.testPercentage || 0)
|
|
422
190
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function findDuplicateTestNames() {
|
|
5
|
+
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(testStatusFile)) {
|
|
8
|
+
console.error('test-status.txt not found');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
13
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
14
|
+
|
|
15
|
+
const testNameToFiles = {};
|
|
16
|
+
|
|
17
|
+
lines.forEach(line => {
|
|
18
|
+
const parts = line.split(' | ');
|
|
19
|
+
if (parts.length < 5) return;
|
|
20
|
+
|
|
21
|
+
const testName = parts[2].trim();
|
|
22
|
+
const filePath = parts[4].trim();
|
|
23
|
+
|
|
24
|
+
if (!testNameToFiles[testName]) {
|
|
25
|
+
testNameToFiles[testName] = new Set();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
testNameToFiles[testName].add(filePath);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const duplicates = {};
|
|
32
|
+
|
|
33
|
+
Object.entries(testNameToFiles).forEach(([testName, files]) => {
|
|
34
|
+
if (files.size > 1) {
|
|
35
|
+
duplicates[testName] = Array.from(files);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (Object.keys(duplicates).length > 0) {
|
|
40
|
+
console.warn('\n\x1b[33m[WARNING] Duplicate scenarios names found: This will effect your reporting');
|
|
41
|
+
Object.entries(duplicates).forEach(([testName, files]) => {
|
|
42
|
+
console.log(`\x1b[33m"${testName}" exists in:`);
|
|
43
|
+
files.forEach(file => {
|
|
44
|
+
console.log(` - ${file}`);
|
|
45
|
+
});
|
|
46
|
+
console.log('');
|
|
47
|
+
});
|
|
48
|
+
console.log("\x1b[0m");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return duplicates;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {findDuplicateTestNames}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function getExecutor() {
|
|
2
|
+
|
|
3
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
4
|
+
return {
|
|
5
|
+
name: "GitHub Actions",
|
|
6
|
+
type: "github",
|
|
7
|
+
buildName: `Workflow #${process.env.GITHUB_RUN_NUMBER}`,
|
|
8
|
+
buildOrder: Number(process.env.GITHUB_RUN_NUMBER),
|
|
9
|
+
buildUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
} else if (process.env.JENKINS_HOME) {
|
|
14
|
+
return {
|
|
15
|
+
name: "Jenkins",
|
|
16
|
+
type: "jenkins",
|
|
17
|
+
buildName: process.env.JOB_NAME || "Manual Run",
|
|
18
|
+
buildOrder: Number(process.env.BUILD_NUMBER) || 1,
|
|
19
|
+
buildUrl: process.env.BUILD_URL || ""
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
} else if (process.env.CI_PIPELINE_ID) {
|
|
24
|
+
return {
|
|
25
|
+
name: "GitLab CI",
|
|
26
|
+
type: "gitlab",
|
|
27
|
+
buildName: `Pipeline #${process.env.CI_PIPELINE_IID}`,
|
|
28
|
+
buildOrder: Number(process.env.CI_PIPELINE_IID) || 1,
|
|
29
|
+
buildUrl: process.env.CI_PIPELINE_URL || ""
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
} else if (process.env.BITBUCKET_BUILD_NUMBER) {
|
|
34
|
+
return {
|
|
35
|
+
name: "Bitbucket Pipelines",
|
|
36
|
+
type: "bitbucket",
|
|
37
|
+
buildName: `Build #${process.env.BITBUCKET_BUILD_NUMBER}`,
|
|
38
|
+
buildOrder: Number(process.env.BITBUCKET_BUILD_NUMBER),
|
|
39
|
+
buildUrl: process.env.BITBUCKET_BUILD_URL || ""
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
} else if (process.env.CIRCLE_WORKFLOW_ID) {
|
|
44
|
+
return {
|
|
45
|
+
name: "CircleCI",
|
|
46
|
+
type: "circleci",
|
|
47
|
+
buildName: `Workflow #${process.env.CIRCLE_WORKFLOW_ID}`,
|
|
48
|
+
buildOrder: Number(process.env.CIRCLE_BUILD_NUM) || 1,
|
|
49
|
+
buildUrl: process.env.CIRCLE_BUILD_URL || ""
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
} else if (process.env.BUILD_BUILDID) {
|
|
54
|
+
return {
|
|
55
|
+
name: "Azure Pipelines",
|
|
56
|
+
type: "azure",
|
|
57
|
+
buildName: `Build #${process.env.BUILD_BUILDID}`,
|
|
58
|
+
buildOrder: Number(process.env.BUILD_BUILDID) || 1,
|
|
59
|
+
buildUrl: process.env.BUILD_BUILDURI || ""
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
} else if (process.env.BUILD_NUMBER && process.env.TEAMCITY_VERSION) {
|
|
64
|
+
return {
|
|
65
|
+
name: "TeamCity",
|
|
66
|
+
type: "teamcity",
|
|
67
|
+
buildName: `Build #${process.env.BUILD_NUMBER}`,
|
|
68
|
+
buildOrder: Number(process.env.BUILD_NUMBER) || 1,
|
|
69
|
+
buildUrl: process.env.BUILD_URL || ""
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
} else if (process.env.TRAVIS_BUILD_NUMBER) {
|
|
74
|
+
return {
|
|
75
|
+
name: "Travis CI",
|
|
76
|
+
type: "travis",
|
|
77
|
+
buildName: `Build #${process.env.TRAVIS_BUILD_NUMBER}`,
|
|
78
|
+
buildOrder: Number(process.env.TRAVIS_BUILD_NUMBER) || 1,
|
|
79
|
+
buildUrl: process.env.TRAVIS_BUILD_WEB_URL || ""
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
} else if (process.env.bamboo_buildNumber) {
|
|
84
|
+
return {
|
|
85
|
+
name: "Bamboo",
|
|
86
|
+
type: "bamboo",
|
|
87
|
+
buildName: `Build #${process.env.bamboo_buildNumber}`,
|
|
88
|
+
buildOrder: Number(process.env.bamboo_buildNumber) || 1,
|
|
89
|
+
buildUrl: process.env.bamboo_resultsUrl || ""
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
} else {
|
|
94
|
+
return {
|
|
95
|
+
name: "Local Run",
|
|
96
|
+
type: "local",
|
|
97
|
+
buildName: "Manual Execution",
|
|
98
|
+
buildOrder: 1,
|
|
99
|
+
buildUrl: ""
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { getExecutor };
|
|
@@ -39,16 +39,29 @@ function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMi
|
|
|
39
39
|
const logoBase64 = logoBuffer.toString("base64");
|
|
40
40
|
const logoDataUrl = `data:${logoMime};base64,${logoBase64}`;
|
|
41
41
|
|
|
42
|
+
const testPercentage = cucumberConfig.default?.testPercentage ?? 0;
|
|
43
|
+
let testCoverageWidgetCss = "";
|
|
44
|
+
|
|
45
|
+
if (testPercentage > 0) {
|
|
46
|
+
const { testCoverageCalculator } = require("./testCoverageCalculator");
|
|
47
|
+
const testCoverage = testCoverageCalculator();
|
|
48
|
+
|
|
49
|
+
if (testCoverage) {
|
|
50
|
+
const meetsThreshold = testCoverage.percentage >= testPercentage;
|
|
51
|
+
|
|
52
|
+
testCoverageWidgetCss = generateTestCoverageWidgetCss(testCoverage, testPercentage, meetsThreshold);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
if (cucumberConfig.report.singleFileReport) {
|
|
43
57
|
const htmlPath = path.resolve(__dirname, "../../../../../report/index.html");
|
|
44
58
|
const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
|
|
45
59
|
|
|
46
|
-
const dynamicCss = generateCss(report, today, reportName, logoDataUrl);
|
|
60
|
+
const dynamicCss = generateCss(report, today, reportName, logoDataUrl) + (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
|
|
47
61
|
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
48
62
|
const cssBase64 = Buffer.from(modifiedCss).toString("base64");
|
|
49
63
|
const cssDataUrl = `data:text/css;base64,${cssBase64}`;
|
|
50
64
|
|
|
51
|
-
|
|
52
65
|
updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssDataUrl);
|
|
53
66
|
|
|
54
67
|
} else {
|
|
@@ -57,13 +70,10 @@ function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMi
|
|
|
57
70
|
const reportDir = path.resolve(__dirname, "../../../../../report");
|
|
58
71
|
const reportCssPath = path.join(reportDir, "styles.css");
|
|
59
72
|
|
|
60
|
-
|
|
61
73
|
const logoDest = path.join(reportDir, logoFilename);
|
|
62
74
|
fs.writeFileSync(logoDest, logoBuffer);
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
const dynamicCss = generateCss(report, today, reportName, logoFilename);
|
|
66
|
-
|
|
76
|
+
const dynamicCss = generateCss(report, today, reportName, logoFilename) + (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
|
|
67
77
|
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
68
78
|
fs.writeFileSync(reportCssPath, modifiedCss, "utf8");
|
|
69
79
|
|
|
@@ -71,6 +81,193 @@ function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMi
|
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
function generateTestCoverageWidgetCss(testCoverage, testPercentage, meetsThreshold) {
|
|
85
|
+
const fill = Math.min(testCoverage.percentage, 100);
|
|
86
|
+
const fillPct = fill.toFixed(4);
|
|
87
|
+
const pctLabel = testCoverage.percentage.toFixed(2);
|
|
88
|
+
const statusColor = meetsThreshold ? "#4caf50" : "#f44336";
|
|
89
|
+
const statusBg = meetsThreshold ? "rgba(76,175,80,.13)" : "rgba(244,67,54,.13)";
|
|
90
|
+
const statusVerb = meetsThreshold ? "passed" : "failed";
|
|
91
|
+
const subtitleTxt = `${testCoverage.passed} / ${testCoverage.totalTests} passed`;
|
|
92
|
+
const statusLine = `Tests ${statusVerb} \u2014 required ${testPercentage}% with ${pctLabel}%`;
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const r1 = (fill * 0.35).toFixed(2);
|
|
96
|
+
const r2 = (fill * 0.60).toFixed(2);
|
|
97
|
+
const tm = testPercentage;
|
|
98
|
+
|
|
99
|
+
const barGradient = `linear-gradient(to right, #f44336 0%, #ff9800 35%, #ffeb3b 60%, #4caf50 100%)`;
|
|
100
|
+
const pointerX = `${fillPct}%`;
|
|
101
|
+
const svgLabels = [
|
|
102
|
+
{ val: "0", x: "0%", anchor: "start" },
|
|
103
|
+
{ val: "20", x: "20%", anchor: "middle" },
|
|
104
|
+
{ val: "40", x: "40%", anchor: "middle" },
|
|
105
|
+
{ val: "60", x: "60%", anchor: "middle" },
|
|
106
|
+
{ val: "80", x: "80%", anchor: "middle" },
|
|
107
|
+
{ val: "100", x: "100%", anchor: "end" },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const labelNodes = svgLabels
|
|
111
|
+
.map(l => `<text x="${l.x}" y="10" text-anchor="${l.anchor}" font-family="sans-serif" font-size="10" fill="#bbb">${l.val}</text>`)
|
|
112
|
+
.join("");
|
|
113
|
+
|
|
114
|
+
const pointerColor = meetsThreshold ? "#4caf50" : "#f44336";
|
|
115
|
+
const px = (fill * 10).toFixed(1);
|
|
116
|
+
|
|
117
|
+
const labelPointerSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="18">
|
|
118
|
+
<text x="${fillPct}%" y="14" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="${pointerColor}">${pctLabel}%</text>
|
|
119
|
+
</svg>`;
|
|
120
|
+
|
|
121
|
+
const stemSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 18" preserveAspectRatio="none" width="100%" height="18">
|
|
122
|
+
<line x1="${px}" y1="6" x2="${px}" y2="10" stroke="${pointerColor}" stroke-width="6"/>
|
|
123
|
+
<polygon points="${parseFloat(px)-12},8 ${parseFloat(px)+12},8 ${parseFloat(px)},18" fill="${pointerColor}"/>
|
|
124
|
+
</svg>`;
|
|
125
|
+
|
|
126
|
+
const labelPointerB64 = Buffer.from(labelPointerSvg).toString("base64");
|
|
127
|
+
const labelPointerDataUrl = `data:image/svg+xml;base64,${labelPointerB64}`;
|
|
128
|
+
const stemB64 = Buffer.from(stemSvg).toString("base64");
|
|
129
|
+
const stemDataUrl = `data:image/svg+xml;base64,${stemB64}`;
|
|
130
|
+
|
|
131
|
+
const pointerDataUrl = stemDataUrl;
|
|
132
|
+
const pointerLabelDataUrl = labelPointerDataUrl;
|
|
133
|
+
|
|
134
|
+
const labelSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="14">${labelNodes}</svg>`;
|
|
135
|
+
|
|
136
|
+
const svgB64 = Buffer.from(labelSvg).toString("base64");
|
|
137
|
+
const svgDataUrl = `data:image/svg+xml;base64,${svgB64}`;
|
|
138
|
+
|
|
139
|
+
const CARD_H = 180;
|
|
140
|
+
const TITLE_PAD = 14;
|
|
141
|
+
const POINTER_FROM_CARD = 62;
|
|
142
|
+
const BAR_FROM_CARD = 98;
|
|
143
|
+
const LABEL_FROM_CARD = 116;
|
|
144
|
+
const STATUS_FROM_CARD = 138;
|
|
145
|
+
|
|
146
|
+
return `
|
|
147
|
+
/* ── ARTES TEST COVERAGE WIDGET ─────────────────────────────────────────── */
|
|
148
|
+
|
|
149
|
+
[data-id="summary"] {
|
|
150
|
+
margin-bottom: ${CARD_H + 22}px !important;
|
|
151
|
+
position: relative;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* 1. Card shell + "TEST COVERAGE" title — 21px black */
|
|
155
|
+
[data-id="summary"]::after {
|
|
156
|
+
content: 'TEST COVERAGE';
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: calc(100% + 10px);
|
|
159
|
+
left: 0;
|
|
160
|
+
right: 0;
|
|
161
|
+
height: ${CARD_H}px;
|
|
162
|
+
background: #fff;
|
|
163
|
+
border-radius: 3px;
|
|
164
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.15);
|
|
165
|
+
padding: ${TITLE_PAD}px 16px 0;
|
|
166
|
+
box-sizing: border-box;
|
|
167
|
+
font-size: 21px;
|
|
168
|
+
font-weight: 100;
|
|
169
|
+
color: #000;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
line-height: 1.4;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
z-index: 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* 2. "1 / 1 passed 100.00%" — subtitle + percentage on same line */
|
|
177
|
+
[data-id="summary"] .widget__body > div {
|
|
178
|
+
position: relative;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[data-id="summary"] .widget__body > div::before {
|
|
182
|
+
content: '${subtitleTxt} ${pctLabel}%';
|
|
183
|
+
position: absolute;
|
|
184
|
+
top: calc(100% + 10px + 46px);
|
|
185
|
+
left: 0;
|
|
186
|
+
font-size: 14px;
|
|
187
|
+
font-weight: 400;
|
|
188
|
+
color: #999;
|
|
189
|
+
line-height: 1;
|
|
190
|
+
letter-spacing: 0;
|
|
191
|
+
pointer-events: none;
|
|
192
|
+
z-index: 2;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* 3. Pointer + threshold marker layered in one element */
|
|
196
|
+
[data-id="summary"] .widget__body > div > *:first-child {
|
|
197
|
+
position: relative;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
[data-id="summary"] .widget__body > div > *:first-child::after {
|
|
201
|
+
content: '';
|
|
202
|
+
position: absolute;
|
|
203
|
+
top: calc(100% + 10px + ${POINTER_FROM_CARD}px);
|
|
204
|
+
left: 16px;
|
|
205
|
+
right: 16px;
|
|
206
|
+
height: 36px;
|
|
207
|
+
background-image: url("${pointerLabelDataUrl}"), url("${pointerDataUrl}");
|
|
208
|
+
background-repeat: no-repeat;
|
|
209
|
+
background-size: 100% 50%, 100% 50%;
|
|
210
|
+
background-position: top, bottom;
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
z-index: 3;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* 4. SVG label row — perfectly aligned under bar via background-image */
|
|
216
|
+
[data-id="summary"] .widget__body > div::after {
|
|
217
|
+
content: '';
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: calc(100% + 10px + ${LABEL_FROM_CARD}px);
|
|
220
|
+
left: 16px;
|
|
221
|
+
right: 16px;
|
|
222
|
+
height: 14px;
|
|
223
|
+
background-image: url("${svgDataUrl}");
|
|
224
|
+
background-repeat: no-repeat;
|
|
225
|
+
background-size: 100% 100%;
|
|
226
|
+
pointer-events: none;
|
|
227
|
+
z-index: 2;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* 5. Gradient bar */
|
|
231
|
+
[data-id="summary"] .widget__body {
|
|
232
|
+
position: static;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
[data-id="summary"] .widget__body::before {
|
|
236
|
+
content: '';
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: calc(100% + 10px + ${BAR_FROM_CARD}px);
|
|
239
|
+
left: 16px;
|
|
240
|
+
right: 16px;
|
|
241
|
+
height: 14px;
|
|
242
|
+
border-radius: 7px;
|
|
243
|
+
background: ${barGradient};
|
|
244
|
+
box-shadow: inset 0 1px 3px rgba(0,0,0,.12);
|
|
245
|
+
pointer-events: none;
|
|
246
|
+
z-index: 2;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* 5. Status pill */
|
|
250
|
+
[data-id="summary"] .widget__body::after {
|
|
251
|
+
content: '${statusLine}';
|
|
252
|
+
position: absolute;
|
|
253
|
+
top: calc(100% + 10px + ${STATUS_FROM_CARD}px);
|
|
254
|
+
left: 16px;
|
|
255
|
+
right: 16px;
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
font-weight: 500;
|
|
258
|
+
color: ${statusColor};
|
|
259
|
+
background: ${statusBg};
|
|
260
|
+
padding: 5px 10px;
|
|
261
|
+
border-radius: 3px;
|
|
262
|
+
box-sizing: border-box;
|
|
263
|
+
pointer-events: none;
|
|
264
|
+
z-index: 2;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ── End ARTES TEST COVERAGE WIDGET ─────────────────────────────────────── */
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
74
271
|
function resolveLogoPath(logoConfig) {
|
|
75
272
|
if (!logoConfig) {
|
|
76
273
|
return defaultLogoPath();
|
|
@@ -81,7 +278,7 @@ function resolveLogoPath(logoConfig) {
|
|
|
81
278
|
: path.resolve(process.cwd(), logoConfig);
|
|
82
279
|
|
|
83
280
|
if (!fs.existsSync(resolved)) {
|
|
84
|
-
|
|
281
|
+
// console.warn(`[artes] Warning: logo not found at "${resolved}". Falling back to default logo.`);
|
|
85
282
|
return defaultLogoPath();
|
|
86
283
|
}
|
|
87
284
|
|
|
@@ -111,7 +308,6 @@ function fetchRemoteLogo(url, redirectCount = 0) {
|
|
|
111
308
|
const client = url.startsWith("https://") ? https : http;
|
|
112
309
|
|
|
113
310
|
client.get(url, (res) => {
|
|
114
|
-
|
|
115
311
|
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
116
312
|
return fetchRemoteLogo(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
|
|
117
313
|
}
|
|
@@ -125,7 +321,7 @@ function fetchRemoteLogo(url, redirectCount = 0) {
|
|
|
125
321
|
const mime = contentType.split(";")[0].trim();
|
|
126
322
|
|
|
127
323
|
if (!mime.startsWith("image/")) {
|
|
128
|
-
res.resume();
|
|
324
|
+
res.resume();
|
|
129
325
|
return reject(new Error(`URL did not return an image (Content-Type: "${mime || "unknown"}"). Make sure the URL points directly to an image file.`));
|
|
130
326
|
}
|
|
131
327
|
|
|
@@ -185,7 +381,6 @@ function updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssD
|
|
|
185
381
|
function updateHtml(htmlPath, report, reportName, faviconDataUrl) {
|
|
186
382
|
let html = fs.readFileSync(htmlPath, "utf8");
|
|
187
383
|
html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
|
|
188
|
-
|
|
189
384
|
html = html.replace(/<link rel="icon" href=".*?">/, `<link rel="icon" href="${faviconDataUrl}">`);
|
|
190
385
|
fs.writeFileSync(htmlPath, html, "utf8");
|
|
191
386
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function testCoverageCalculator() {
|
|
5
|
+
|
|
6
|
+
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(testStatusFile)) {
|
|
9
|
+
console.error('test-status.txt not found');
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
14
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
15
|
+
|
|
16
|
+
const map = {};
|
|
17
|
+
const retriedTests = [];
|
|
18
|
+
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
19
|
+
|
|
20
|
+
lines.forEach(line => {
|
|
21
|
+
const parts = line.split(' | ');
|
|
22
|
+
if (parts.length < 5) return;
|
|
23
|
+
|
|
24
|
+
const timestamp = parts[0].trim();
|
|
25
|
+
const status = parts[1].trim();
|
|
26
|
+
const scenario = parts[2].trim();
|
|
27
|
+
const id = parts[3].trim();
|
|
28
|
+
const uri = parts[4].trim();
|
|
29
|
+
|
|
30
|
+
if (!uuidRegex.test(id)) return;
|
|
31
|
+
|
|
32
|
+
if (!map[id]) {
|
|
33
|
+
map[id] = {
|
|
34
|
+
count: 1,
|
|
35
|
+
latest: { status, scenario, timestamp, uri }
|
|
36
|
+
};
|
|
37
|
+
} else {
|
|
38
|
+
map[id].count++;
|
|
39
|
+
if (timestamp > map[id].latest.timestamp) {
|
|
40
|
+
map[id].latest = { status, scenario, timestamp, uri };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let total = 0;
|
|
46
|
+
let notPassed = 0;
|
|
47
|
+
|
|
48
|
+
Object.entries(map).forEach(([id, data]) => {
|
|
49
|
+
total++;
|
|
50
|
+
|
|
51
|
+
if (data.count > 1) {
|
|
52
|
+
retriedTests.push({
|
|
53
|
+
scenario: data.latest.scenario,
|
|
54
|
+
id,
|
|
55
|
+
count: data.count
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (data.latest.status !== 'PASSED') {
|
|
60
|
+
notPassed++;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (retriedTests.length > 0) {
|
|
65
|
+
console.warn('\n\x1b[33mRetried test cases:');
|
|
66
|
+
retriedTests.forEach(t => {
|
|
67
|
+
console.warn(`- "${t.scenario}" ran ${t.count} times`);
|
|
68
|
+
});
|
|
69
|
+
console.log("\x1b[0m");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
percentage: (total - notPassed) / total * 100,
|
|
74
|
+
totalTests: total,
|
|
75
|
+
notPassed,
|
|
76
|
+
passed: total - notPassed,
|
|
77
|
+
latestStatuses: Object.fromEntries(
|
|
78
|
+
Object.entries(map).map(([id, data]) => [
|
|
79
|
+
id,
|
|
80
|
+
data.latest.status
|
|
81
|
+
])
|
|
82
|
+
)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { testCoverageCalculator };
|
package/src/hooks/hooks.js
CHANGED
|
@@ -197,38 +197,37 @@ After(async function ({result, pickle}) {
|
|
|
197
197
|
context.page.url() !== "about:blank"
|
|
198
198
|
) {
|
|
199
199
|
const video = context.page.video();
|
|
200
|
-
if (video) {
|
|
201
|
-
const videoPath = await video.path();
|
|
202
|
-
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
204
|
-
|
|
205
|
-
if (fs.existsSync(videoPath)) {
|
|
206
|
-
const trimmedPath = videoPath.replace('.webm', '-trimmed.webm');
|
|
207
|
-
|
|
208
|
-
const isTimeoutError = result.message?.includes('Error: function timed out, ensure the promise resolves within')
|
|
209
|
-
const webmBuffer = fs.readFileSync(videoPath);
|
|
210
|
-
await allure.attachment("Screenrecord", webmBuffer, "video/webm");
|
|
211
|
-
if (isTimeoutError) {
|
|
212
|
-
const duration = parseFloat(
|
|
213
|
-
execSync(`"${ffprobe.path}" -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`).toString().trim()
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const timeoutSeconds = cucumberConfig.default.timeout / 1000;
|
|
217
|
-
const newDuration = Math.max(duration - timeoutSeconds + 3, 1);
|
|
218
|
-
|
|
219
|
-
execSync(`"${ffmpegPath}" -loglevel quiet -i "${videoPath}" -t ${newDuration} -c copy "${trimmedPath}" -y`);
|
|
220
|
-
|
|
221
|
-
const webmBuffer = fs.readFileSync(trimmedPath);
|
|
222
|
-
await allure.attachment("Screenrecord", webmBuffer, "video/webm");
|
|
223
|
-
} else {
|
|
224
|
-
|
|
225
|
-
const webmBuffer = fs.readFileSync(videoPath);
|
|
226
|
-
await allure.attachment("Screenrecord", webmBuffer, "video/webm");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
200
|
|
|
201
|
+
if (video) {
|
|
202
|
+
const videoPath = await video.path();
|
|
203
|
+
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(videoPath)) {
|
|
207
|
+
const trimmedPath = videoPath.replace('.webm', '-trimmed.webm');
|
|
208
|
+
|
|
209
|
+
const isTimeoutError = result.message?.includes('Error: function timed out, ensure the promise resolves within');
|
|
210
|
+
|
|
211
|
+
if (isTimeoutError) {
|
|
212
|
+
const duration = parseFloat(
|
|
213
|
+
execSync(`"${ffprobe.path}" -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`).toString().trim()
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const timeoutSeconds = cucumberConfig.default.timeout / 1000;
|
|
217
|
+
const newDuration = Math.max(duration - timeoutSeconds + 3, 1);
|
|
218
|
+
|
|
219
|
+
execSync(`"${ffmpegPath}" -loglevel quiet -i "${videoPath}" -t ${newDuration} -c copy "${trimmedPath}" -y`);
|
|
220
|
+
|
|
221
|
+
const webmBuffer = fs.readFileSync(trimmedPath);
|
|
222
|
+
await allure.attachment("Screenrecord", webmBuffer, "video/webm");
|
|
223
|
+
} else {
|
|
224
|
+
const webmBuffer = fs.readFileSync(videoPath);
|
|
225
|
+
await allure.attachment("Screenrecord", webmBuffer, "video/webm");
|
|
230
226
|
}
|
|
231
227
|
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
}
|
|
232
231
|
});
|
|
233
232
|
|
|
234
233
|
AfterAll(async () => {
|
|
File without changes
|