artes 1.5.2 → 1.5.3
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 +221 -9
- 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,37 @@ 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
|
+
if (meetsThreshold) {
|
|
53
|
+
console.log(`✅ Tests passed required ${testPercentage}% success rate with ${testCoverage.percentage.toFixed(2)}%!`);
|
|
54
|
+
process.env.EXIT_CODE = parseInt(0, 10);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`❌ Tests failed required ${testPercentage}% success rate with ${testCoverage.percentage.toFixed(2)}%!`);
|
|
57
|
+
process.env.EXIT_CODE = parseInt(1, 10);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
testCoverageWidgetCss = generateTestCoverageWidgetCss(testCoverage, testPercentage, meetsThreshold);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
if (cucumberConfig.report.singleFileReport) {
|
|
43
65
|
const htmlPath = path.resolve(__dirname, "../../../../../report/index.html");
|
|
44
66
|
const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
|
|
45
67
|
|
|
46
|
-
const dynamicCss = generateCss(report, today, reportName, logoDataUrl);
|
|
68
|
+
const dynamicCss = generateCss(report, today, reportName, logoDataUrl) + (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
|
|
47
69
|
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
48
70
|
const cssBase64 = Buffer.from(modifiedCss).toString("base64");
|
|
49
71
|
const cssDataUrl = `data:text/css;base64,${cssBase64}`;
|
|
50
72
|
|
|
51
|
-
|
|
52
73
|
updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssDataUrl);
|
|
53
74
|
|
|
54
75
|
} else {
|
|
@@ -57,13 +78,10 @@ function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMi
|
|
|
57
78
|
const reportDir = path.resolve(__dirname, "../../../../../report");
|
|
58
79
|
const reportCssPath = path.join(reportDir, "styles.css");
|
|
59
80
|
|
|
60
|
-
|
|
61
81
|
const logoDest = path.join(reportDir, logoFilename);
|
|
62
82
|
fs.writeFileSync(logoDest, logoBuffer);
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
const dynamicCss = generateCss(report, today, reportName, logoFilename);
|
|
66
|
-
|
|
84
|
+
const dynamicCss = generateCss(report, today, reportName, logoFilename) + (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
|
|
67
85
|
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
68
86
|
fs.writeFileSync(reportCssPath, modifiedCss, "utf8");
|
|
69
87
|
|
|
@@ -71,6 +89,202 @@ function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMi
|
|
|
71
89
|
}
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
function generateTestCoverageWidgetCss(testCoverage, testPercentage, meetsThreshold) {
|
|
93
|
+
const fill = Math.min(testCoverage.percentage, 100);
|
|
94
|
+
const fillPct = fill.toFixed(4);
|
|
95
|
+
const pctLabel = testCoverage.percentage.toFixed(2);
|
|
96
|
+
const statusColor = meetsThreshold ? "#4caf50" : "#f44336";
|
|
97
|
+
const statusBg = meetsThreshold ? "rgba(76,175,80,.13)" : "rgba(244,67,54,.13)";
|
|
98
|
+
const statusVerb = meetsThreshold ? "passed" : "failed";
|
|
99
|
+
const subtitleTxt = `${testCoverage.passed} / ${testCoverage.totalTests} passed`;
|
|
100
|
+
const statusLine = `Tests ${statusVerb} \u2014 required ${testPercentage}% with ${pctLabel}%`;
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
const r1 = (fill * 0.35).toFixed(2);
|
|
104
|
+
const r2 = (fill * 0.60).toFixed(2);
|
|
105
|
+
const tm = testPercentage;
|
|
106
|
+
|
|
107
|
+
const barGradient = `
|
|
108
|
+
linear-gradient(to right,
|
|
109
|
+
transparent calc(${tm}% - 1.5px),
|
|
110
|
+
rgba(255,255,255,.95) calc(${tm}% - 1.5px),
|
|
111
|
+
rgba(255,255,255,.95) calc(${tm}% + 1.5px),
|
|
112
|
+
transparent calc(${tm}% + 1.5px)
|
|
113
|
+
),
|
|
114
|
+
linear-gradient(to right, #f44336 0%, #ff9800 35%, #ffeb3b 60%, #4caf50 100%)
|
|
115
|
+
`.trim();
|
|
116
|
+
|
|
117
|
+
const pointerX = `${fillPct}%`;
|
|
118
|
+
const svgLabels = [
|
|
119
|
+
{ val: "0", x: "0%", anchor: "start" },
|
|
120
|
+
{ val: "20", x: "20%", anchor: "middle" },
|
|
121
|
+
{ val: "40", x: "40%", anchor: "middle" },
|
|
122
|
+
{ val: "60", x: "60%", anchor: "middle" },
|
|
123
|
+
{ val: "80", x: "80%", anchor: "middle" },
|
|
124
|
+
{ val: "100", x: "100%", anchor: "end" },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const labelNodes = svgLabels
|
|
128
|
+
.map(l => `<text x="${l.x}" y="10" text-anchor="${l.anchor}" font-family="sans-serif" font-size="10" fill="#bbb">${l.val}</text>`)
|
|
129
|
+
.join("");
|
|
130
|
+
const pointerColor = meetsThreshold ? "#4caf50" : "#f44336";
|
|
131
|
+
const px = (fill * 10).toFixed(1); // 0–1000 units mapping to 0–100%
|
|
132
|
+
|
|
133
|
+
const labelPointerSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="18">
|
|
134
|
+
<text x="${fillPct}%" y="14" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="${pointerColor}">${pctLabel}%</text>
|
|
135
|
+
</svg>`;
|
|
136
|
+
|
|
137
|
+
const stemSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 18" preserveAspectRatio="none" width="100%" height="18">
|
|
138
|
+
<line x1="${px}" y1="0" x2="${px}" y2="10" stroke="${pointerColor}" stroke-width="6"/>
|
|
139
|
+
<polygon points="${parseFloat(px)-12},8 ${parseFloat(px)+12},8 ${parseFloat(px)},18" fill="${pointerColor}"/>
|
|
140
|
+
</svg>`;
|
|
141
|
+
|
|
142
|
+
const labelPointerB64 = Buffer.from(labelPointerSvg).toString("base64");
|
|
143
|
+
const labelPointerDataUrl = `data:image/svg+xml;base64,${labelPointerB64}`;
|
|
144
|
+
const stemB64 = Buffer.from(stemSvg).toString("base64");
|
|
145
|
+
const stemDataUrl = `data:image/svg+xml;base64,${stemB64}`;
|
|
146
|
+
|
|
147
|
+
const pointerDataUrl = stemDataUrl;
|
|
148
|
+
const pointerLabelDataUrl = labelPointerDataUrl;
|
|
149
|
+
|
|
150
|
+
const labelSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="14">${labelNodes}</svg>`;
|
|
151
|
+
|
|
152
|
+
const svgB64 = Buffer.from(labelSvg).toString("base64");
|
|
153
|
+
const svgDataUrl = `data:image/svg+xml;base64,${svgB64}`;
|
|
154
|
+
|
|
155
|
+
const CARD_H = 180;
|
|
156
|
+
const TITLE_PAD = 14;
|
|
157
|
+
const POINTER_FROM_CARD = 62;
|
|
158
|
+
const BAR_FROM_CARD = 98;
|
|
159
|
+
const LABEL_FROM_CARD = 116;
|
|
160
|
+
const STATUS_FROM_CARD = 138;
|
|
161
|
+
|
|
162
|
+
return `
|
|
163
|
+
/* ── ARTES TEST COVERAGE WIDGET ─────────────────────────────────────────── */
|
|
164
|
+
|
|
165
|
+
[data-id="summary"] {
|
|
166
|
+
margin-bottom: ${CARD_H + 22}px !important;
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* 1. Card shell + "TEST COVERAGE" title — 21px black */
|
|
171
|
+
[data-id="summary"]::after {
|
|
172
|
+
content: 'TEST COVERAGE';
|
|
173
|
+
position: absolute;
|
|
174
|
+
top: calc(100% + 10px);
|
|
175
|
+
left: 0;
|
|
176
|
+
right: 0;
|
|
177
|
+
height: ${CARD_H}px;
|
|
178
|
+
background: #fff;
|
|
179
|
+
border-radius: 3px;
|
|
180
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.15);
|
|
181
|
+
padding: ${TITLE_PAD}px 16px 0;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
font-size: 21px;
|
|
184
|
+
font-weight: 100;
|
|
185
|
+
color: #000;
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
line-height: 1.4;
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
z-index: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* 2. "1 / 1 passed 100.00%" — subtitle + percentage on same line */
|
|
193
|
+
[data-id="summary"] .widget__body > div {
|
|
194
|
+
position: relative;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[data-id="summary"] .widget__body > div::before {
|
|
198
|
+
content: '${subtitleTxt} ${pctLabel}%';
|
|
199
|
+
position: absolute;
|
|
200
|
+
top: calc(100% + 10px + 46px);
|
|
201
|
+
left: 0;
|
|
202
|
+
font-size: 14px;
|
|
203
|
+
font-weight: 400;
|
|
204
|
+
color: #999;
|
|
205
|
+
line-height: 1;
|
|
206
|
+
letter-spacing: 0;
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
z-index: 2;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* 3. Pointer — label SVG on top, stem+triangle SVG below, layered via multiple backgrounds */
|
|
212
|
+
[data-id="summary"] .widget__body > div > *:first-child {
|
|
213
|
+
position: relative;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
[data-id="summary"] .widget__body > div > *:first-child::after {
|
|
217
|
+
content: '';
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: calc(100% + 10px + ${POINTER_FROM_CARD}px);
|
|
220
|
+
left: 16px;
|
|
221
|
+
right: 16px;
|
|
222
|
+
height: 36px;
|
|
223
|
+
/* label on top (18px), stem+triangle below (18px) */
|
|
224
|
+
background-image: url("${pointerLabelDataUrl}"), url("${pointerDataUrl}");
|
|
225
|
+
background-repeat: no-repeat, no-repeat;
|
|
226
|
+
background-size: 100% 50%, 100% 50%;
|
|
227
|
+
background-position: top, bottom;
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
z-index: 3;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* 4. SVG label row — perfectly aligned under bar via background-image */
|
|
233
|
+
[data-id="summary"] .widget__body > div::after {
|
|
234
|
+
content: '';
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: calc(100% + 10px + ${LABEL_FROM_CARD}px);
|
|
237
|
+
left: 16px;
|
|
238
|
+
right: 16px;
|
|
239
|
+
height: 14px;
|
|
240
|
+
background-image: url("${svgDataUrl}");
|
|
241
|
+
background-repeat: no-repeat;
|
|
242
|
+
background-size: 100% 100%;
|
|
243
|
+
pointer-events: none;
|
|
244
|
+
z-index: 2;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* 5. Gradient bar */
|
|
248
|
+
[data-id="summary"] .widget__body {
|
|
249
|
+
position: static;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
[data-id="summary"] .widget__body::before {
|
|
253
|
+
content: '';
|
|
254
|
+
position: absolute;
|
|
255
|
+
top: calc(100% + 10px + ${BAR_FROM_CARD}px);
|
|
256
|
+
left: 16px;
|
|
257
|
+
right: 16px;
|
|
258
|
+
height: 14px;
|
|
259
|
+
border-radius: 7px;
|
|
260
|
+
background: ${barGradient};
|
|
261
|
+
box-shadow: inset 0 1px 3px rgba(0,0,0,.12);
|
|
262
|
+
pointer-events: none;
|
|
263
|
+
z-index: 2;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* 5. Status pill */
|
|
267
|
+
[data-id="summary"] .widget__body::after {
|
|
268
|
+
content: '${statusLine}';
|
|
269
|
+
position: absolute;
|
|
270
|
+
top: calc(100% + 10px + ${STATUS_FROM_CARD}px);
|
|
271
|
+
left: 16px;
|
|
272
|
+
right: 16px;
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
font-weight: 500;
|
|
275
|
+
color: ${statusColor};
|
|
276
|
+
background: ${statusBg};
|
|
277
|
+
padding: 5px 10px;
|
|
278
|
+
border-radius: 3px;
|
|
279
|
+
box-sizing: border-box;
|
|
280
|
+
pointer-events: none;
|
|
281
|
+
z-index: 2;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ── End ARTES TEST COVERAGE WIDGET ─────────────────────────────────────── */
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
74
288
|
function resolveLogoPath(logoConfig) {
|
|
75
289
|
if (!logoConfig) {
|
|
76
290
|
return defaultLogoPath();
|
|
@@ -111,7 +325,6 @@ function fetchRemoteLogo(url, redirectCount = 0) {
|
|
|
111
325
|
const client = url.startsWith("https://") ? https : http;
|
|
112
326
|
|
|
113
327
|
client.get(url, (res) => {
|
|
114
|
-
|
|
115
328
|
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
116
329
|
return fetchRemoteLogo(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
|
|
117
330
|
}
|
|
@@ -125,7 +338,7 @@ function fetchRemoteLogo(url, redirectCount = 0) {
|
|
|
125
338
|
const mime = contentType.split(";")[0].trim();
|
|
126
339
|
|
|
127
340
|
if (!mime.startsWith("image/")) {
|
|
128
|
-
res.resume();
|
|
341
|
+
res.resume();
|
|
129
342
|
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
343
|
}
|
|
131
344
|
|
|
@@ -185,7 +398,6 @@ function updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssD
|
|
|
185
398
|
function updateHtml(htmlPath, report, reportName, faviconDataUrl) {
|
|
186
399
|
let html = fs.readFileSync(htmlPath, "utf8");
|
|
187
400
|
html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
|
|
188
|
-
|
|
189
401
|
html = html.replace(/<link rel="icon" href=".*?">/, `<link rel="icon" href="${faviconDataUrl}">`);
|
|
190
402
|
fs.writeFileSync(htmlPath, html, "utf8");
|
|
191
403
|
}
|
|
@@ -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
|