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.
@@ -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 = testCoverageCalculation()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artes",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "The simplest way to automate UI and API tests using Cucumber-style steps.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- console.warn(`[artes] Warning: logo not found at "${resolved}". Falling back to default logo.`);
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 };
@@ -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 () => {