artes 1.7.4 → 1.7.6

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.
Files changed (48) hide show
  1. package/README.md +781 -779
  2. package/assets/styles.css +4 -4
  3. package/cucumber.config.js +253 -253
  4. package/docs/ciExecutors.md +198 -198
  5. package/docs/emulationDevicesList.md +152 -152
  6. package/docs/functionDefinitions.md +2401 -2401
  7. package/docs/stepDefinitions.md +435 -433
  8. package/executer.js +266 -264
  9. package/index.js +50 -50
  10. package/package.json +56 -56
  11. package/src/helper/contextManager/browserManager.js +74 -74
  12. package/src/helper/contextManager/requestManager.js +23 -23
  13. package/src/helper/controller/elementController.js +210 -210
  14. package/src/helper/controller/findDuplicateTestNames.js +69 -69
  15. package/src/helper/controller/getEnvInfo.js +94 -94
  16. package/src/helper/controller/getExecutor.js +109 -109
  17. package/src/helper/controller/pomCollector.js +83 -83
  18. package/src/helper/controller/reportCustomizer.js +485 -485
  19. package/src/helper/controller/screenComparer.js +97 -108
  20. package/src/helper/controller/status-formatter.js +137 -137
  21. package/src/helper/controller/testCoverageCalculator.js +111 -111
  22. package/src/helper/executers/cleaner.js +23 -23
  23. package/src/helper/executers/exporter.js +19 -19
  24. package/src/helper/executers/helper.js +193 -191
  25. package/src/helper/executers/projectCreator.js +226 -222
  26. package/src/helper/executers/reportGenerator.js +91 -91
  27. package/src/helper/executers/testRunner.js +28 -28
  28. package/src/helper/executers/versionChecker.js +31 -31
  29. package/src/helper/imports/commons.js +65 -65
  30. package/src/helper/stepFunctions/APIActions.js +495 -495
  31. package/src/helper/stepFunctions/assertions.js +986 -986
  32. package/src/helper/stepFunctions/browserActions.js +87 -87
  33. package/src/helper/stepFunctions/elementInteractions.js +60 -60
  34. package/src/helper/stepFunctions/exporter.js +19 -19
  35. package/src/helper/stepFunctions/frameActions.js +72 -72
  36. package/src/helper/stepFunctions/keyboardActions.js +66 -66
  37. package/src/helper/stepFunctions/mouseActions.js +84 -84
  38. package/src/helper/stepFunctions/pageActions.js +43 -43
  39. package/src/hooks/context.js +15 -15
  40. package/src/hooks/hooks.js +287 -279
  41. package/src/stepDefinitions/API.steps.js +310 -310
  42. package/src/stepDefinitions/assertions.steps.js +1303 -1280
  43. package/src/stepDefinitions/browser.steps.js +74 -74
  44. package/src/stepDefinitions/frameActions.steps.js +76 -76
  45. package/src/stepDefinitions/keyboardActions.steps.js +264 -264
  46. package/src/stepDefinitions/mouseActions.steps.js +378 -378
  47. package/src/stepDefinitions/page.steps.js +71 -71
  48. package/src/stepDefinitions/random.steps.js +191 -191
@@ -1,485 +1,485 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const https = require("https");
4
- const http = require("http");
5
-
6
- const CSS_START_MARKER = "/* ARTES_DYNAMIC_START */";
7
- const CSS_END_MARKER = "/* ARTES_DYNAMIC_END */";
8
-
9
- function reportCustomizer() {
10
- delete require.cache[require.resolve("../../../cucumber.config.js")];
11
- const cucumberConfig = require("../../../cucumber.config.js");
12
-
13
- const report = cucumberConfig.report;
14
- const today = new Date().toLocaleDateString("en-US", {
15
- month: "numeric",
16
- day: "numeric",
17
- year: "numeric",
18
- });
19
- const reportName =
20
- typeof report.reportName === "string"
21
- ? report.reportName
22
- : report.reportName.name || "";
23
-
24
- const { buffer: faviconBuffer, mime: faviconMime } =
25
- readFileAsBuffer(defaultLogoPath());
26
- const faviconDataUrl = `data:${faviconMime};base64,${faviconBuffer.toString("base64")}`;
27
-
28
- if (isRemoteUrl(report.logo)) {
29
- return fetchRemoteLogo(report.logo)
30
- .catch((err) => {
31
- console.warn(
32
- `[artes] Warning: failed to fetch logo from "${report.logo}": ${err.message}. Falling back to default logo.`,
33
- );
34
- return readFileAsBuffer(defaultLogoPath());
35
- })
36
- .then(({ buffer, mime, filename }) => {
37
- applyLogo(
38
- cucumberConfig,
39
- report,
40
- today,
41
- reportName,
42
- buffer,
43
- mime,
44
- filename,
45
- faviconDataUrl,
46
- );
47
- });
48
- }
49
-
50
- const logoSrc = resolveLogoPath(report.logo);
51
- const logoExt = path.extname(logoSrc).replace(".", "").toLowerCase();
52
- const logoMime = logoExt === "svg" ? "image/svg+xml" : `image/${logoExt}`;
53
- const logoBuffer = fs.readFileSync(logoSrc);
54
- applyLogo(
55
- cucumberConfig,
56
- report,
57
- today,
58
- reportName,
59
- logoBuffer,
60
- logoMime,
61
- path.basename(logoSrc),
62
- faviconDataUrl,
63
- );
64
- }
65
-
66
- function applyLogo(
67
- cucumberConfig,
68
- report,
69
- today,
70
- reportName,
71
- logoBuffer,
72
- logoMime,
73
- logoFilename,
74
- faviconDataUrl,
75
- ) {
76
- const logoBase64 = logoBuffer.toString("base64");
77
- const logoDataUrl = `data:${logoMime};base64,${logoBase64}`;
78
-
79
- const testPercentage = cucumberConfig.default?.testPercentage ?? 0;
80
- let testCoverageWidgetCss = "";
81
-
82
- if (testPercentage > 0) {
83
- const { testCoverageCalculator } = require("./testCoverageCalculator");
84
- const testCoverage = testCoverageCalculator({ silent: true });
85
-
86
- if (testCoverage) {
87
- const meetsThreshold = testCoverage.percentage >= testPercentage;
88
-
89
- testCoverageWidgetCss = generateTestCoverageWidgetCss(
90
- testCoverage,
91
- testPercentage,
92
- meetsThreshold,
93
- );
94
- }
95
- }
96
-
97
- if (cucumberConfig.report.singleFileReport) {
98
- const htmlPath = path.resolve(
99
- __dirname,
100
- "../../../../../report/index.html",
101
- );
102
- const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
103
-
104
- const dynamicCss =
105
- generateCss(report, today, reportName, logoDataUrl) +
106
- (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
107
- const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
108
- const cssBase64 = Buffer.from(modifiedCss).toString("base64");
109
- const cssDataUrl = `data:text/css;base64,${cssBase64}`;
110
-
111
- updateSingleFileHtml(
112
- htmlPath,
113
- report,
114
- reportName,
115
- faviconDataUrl,
116
- cssDataUrl,
117
- );
118
- } else {
119
- const htmlPath = path.resolve(
120
- __dirname,
121
- "../../../../../report/index.html",
122
- );
123
- const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
124
- const reportDir = path.resolve(__dirname, "../../../../../report");
125
- const reportCssPath = path.join(reportDir, "styles.css");
126
-
127
- const logoDest = path.join(reportDir, logoFilename);
128
- fs.writeFileSync(logoDest, logoBuffer);
129
-
130
- const dynamicCss =
131
- generateCss(report, today, reportName, logoFilename) +
132
- (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
133
- const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
134
- fs.writeFileSync(reportCssPath, modifiedCss, "utf8");
135
-
136
- updateHtml(htmlPath, report, reportName, faviconDataUrl);
137
- }
138
- }
139
-
140
- function generateTestCoverageWidgetCss(
141
- testCoverage,
142
- testPercentage,
143
- meetsThreshold,
144
- ) {
145
- const fill = Math.min(testCoverage.percentage, 100);
146
- const fillPct = fill.toFixed(4);
147
- const pctLabel = testCoverage.percentage.toFixed(2);
148
- const statusColor = meetsThreshold ? "#4caf50" : "#f44336";
149
- const statusBg = meetsThreshold
150
- ? "rgba(76,175,80,.13)"
151
- : "rgba(244,67,54,.13)";
152
- const statusVerb = meetsThreshold ? "passed" : "failed";
153
- const subtitleTxt = `${testCoverage.passed} / ${testCoverage.totalTests} passed`;
154
- const statusLine = `Tests ${statusVerb} \u2014 required ${testPercentage}% with ${pctLabel}%`;
155
-
156
- const barGradient = `linear-gradient(to right, #f44336 0%, #ff9800 35%, #ffeb3b 60%, #4caf50 100%)`;
157
-
158
- const svgLabels = [
159
- { val: "0", x: "0%", anchor: "start" },
160
- { val: "20", x: "20%", anchor: "middle" },
161
- { val: "40", x: "40%", anchor: "middle" },
162
- { val: "60", x: "60%", anchor: "middle" },
163
- { val: "80", x: "80%", anchor: "middle" },
164
- { val: "100", x: "100%", anchor: "end" },
165
- ];
166
-
167
- const labelNodes = svgLabels
168
- .map(
169
- (l) =>
170
- `<text x="${l.x}" y="10" text-anchor="${l.anchor}" font-family="sans-serif" font-size="10" fill="#bbb">${l.val}</text>`,
171
- )
172
- .join("");
173
-
174
- const pointerColor = meetsThreshold ? "#4caf50" : "#f44336";
175
- const px = (fill * 10).toFixed(1);
176
-
177
- const labelPointerSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="18">
178
- <text x="${fillPct}%" y="14" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="${pointerColor}">${pctLabel}%</text>
179
- </svg>`;
180
-
181
- const stemSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 18" preserveAspectRatio="none" width="100%" height="18">
182
- <line x1="${px}" y1="6" x2="${px}" y2="10" stroke="${pointerColor}" stroke-width="6"/>
183
- <polygon points="${parseFloat(px) - 12},8 ${parseFloat(px) + 12},8 ${parseFloat(px)},18" fill="${pointerColor}"/>
184
- </svg>`;
185
-
186
- const labelPointerB64 = Buffer.from(labelPointerSvg).toString("base64");
187
- const labelPointerDataUrl = `data:image/svg+xml;base64,${labelPointerB64}`;
188
- const stemB64 = Buffer.from(stemSvg).toString("base64");
189
- const stemDataUrl = `data:image/svg+xml;base64,${stemB64}`;
190
-
191
- const pointerDataUrl = stemDataUrl;
192
- const pointerLabelDataUrl = labelPointerDataUrl;
193
-
194
- const labelSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="14">${labelNodes}</svg>`;
195
-
196
- const svgB64 = Buffer.from(labelSvg).toString("base64");
197
- const svgDataUrl = `data:image/svg+xml;base64,${svgB64}`;
198
-
199
- const CARD_H = 180;
200
- const TITLE_PAD = 14;
201
- const POINTER_FROM_CARD = 62;
202
- const BAR_FROM_CARD = 98;
203
- const LABEL_FROM_CARD = 116;
204
- const STATUS_FROM_CARD = 138;
205
-
206
- return `
207
- /* ── ARTES TEST COVERAGE WIDGET ─────────────────────────────────────────── */
208
-
209
- [data-id="summary"] {
210
- margin-bottom: ${CARD_H + 22}px !important;
211
- position: relative;
212
- }
213
-
214
- /* 1. Card shell + "TEST COVERAGE" title — 21px black */
215
- [data-id="summary"]::after {
216
- content: 'TEST COVERAGE';
217
- position: absolute;
218
- top: calc(100% + 10px);
219
- left: 0;
220
- right: 0;
221
- height: ${CARD_H}px;
222
- background: #fff;
223
- border-radius: 3px;
224
- box-shadow: 0 1px 3px rgba(0,0,0,.15);
225
- padding: ${TITLE_PAD}px 16px 0;
226
- box-sizing: border-box;
227
- font-size: 21px;
228
- font-weight: 100;
229
- color: #000;
230
- text-transform: uppercase;
231
- line-height: 1.4;
232
- pointer-events: none;
233
- z-index: 1;
234
- }
235
-
236
- /* 2. "1 / 1 passed 100.00%" — subtitle + percentage on same line */
237
- [data-id="summary"] .widget__body > div {
238
- position: relative;
239
- }
240
-
241
- [data-id="summary"] .widget__body > div::before {
242
- content: '${subtitleTxt} ${pctLabel}%';
243
- position: absolute;
244
- top: calc(100% + 10px + 46px);
245
- left: 0;
246
- font-size: 14px;
247
- font-weight: 400;
248
- color: #999;
249
- line-height: 1;
250
- letter-spacing: 0;
251
- pointer-events: none;
252
- z-index: 2;
253
- }
254
-
255
- /* 3. Pointer + threshold marker layered in one element */
256
- [data-id="summary"] .widget__body > div > *:first-child {
257
- position: relative;
258
- }
259
-
260
- [data-id="summary"] .widget__body > div > *:first-child::after {
261
- content: '';
262
- position: absolute;
263
- top: calc(100% + 10px + ${POINTER_FROM_CARD}px);
264
- left: 16px;
265
- right: 16px;
266
- height: 36px;
267
- background-image: url("${pointerLabelDataUrl}"), url("${pointerDataUrl}");
268
- background-repeat: no-repeat;
269
- background-size: 100% 50%, 100% 50%;
270
- background-position: top, bottom;
271
- pointer-events: none;
272
- z-index: 3;
273
- }
274
-
275
- /* 4. SVG label row — perfectly aligned under bar via background-image */
276
- [data-id="summary"] .widget__body > div::after {
277
- content: '';
278
- position: absolute;
279
- top: calc(100% + 10px + ${LABEL_FROM_CARD}px);
280
- left: 16px;
281
- right: 16px;
282
- height: 14px;
283
- background-image: url("${svgDataUrl}");
284
- background-repeat: no-repeat;
285
- background-size: 100% 100%;
286
- pointer-events: none;
287
- z-index: 2;
288
- }
289
-
290
- /* 5. Gradient bar */
291
- [data-id="summary"] .widget__body {
292
- position: static;
293
- }
294
-
295
- [data-id="summary"] .widget__body::before {
296
- content: '';
297
- position: absolute;
298
- top: calc(100% + 10px + ${BAR_FROM_CARD}px);
299
- left: 16px;
300
- right: 16px;
301
- height: 14px;
302
- border-radius: 7px;
303
- background: ${barGradient};
304
- box-shadow: inset 0 1px 3px rgba(0,0,0,.12);
305
- pointer-events: none;
306
- z-index: 2;
307
- }
308
-
309
- /* 5. Status pill */
310
- [data-id="summary"] .widget__body::after {
311
- content: '${statusLine}';
312
- position: absolute;
313
- top: calc(100% + 10px + ${STATUS_FROM_CARD}px);
314
- left: 16px;
315
- right: 16px;
316
- font-size: 12px;
317
- font-weight: 500;
318
- color: ${statusColor};
319
- background: ${statusBg};
320
- padding: 5px 10px;
321
- border-radius: 3px;
322
- box-sizing: border-box;
323
- pointer-events: none;
324
- z-index: 2;
325
- }
326
-
327
- /* ── End ARTES TEST COVERAGE WIDGET ─────────────────────────────────────── */
328
- `;
329
- }
330
-
331
- function resolveLogoPath(logoConfig) {
332
- if (!logoConfig) {
333
- return defaultLogoPath();
334
- }
335
-
336
- const resolved = path.isAbsolute(logoConfig)
337
- ? logoConfig
338
- : path.resolve(process.cwd(), logoConfig);
339
-
340
- if (!fs.existsSync(resolved)) {
341
- // console.warn(`[artes] Warning: logo not found at "${resolved}". Falling back to default logo.`);
342
- return defaultLogoPath();
343
- }
344
-
345
- return resolved;
346
- }
347
-
348
- function defaultLogoPath() {
349
- return path.resolve(
350
- process.cwd(),
351
- "node_modules",
352
- "artes",
353
- "assets",
354
- "logo.png",
355
- );
356
- }
357
-
358
- function isRemoteUrl(logoConfig) {
359
- return (
360
- typeof logoConfig === "string" &&
361
- (logoConfig.startsWith("http://") || logoConfig.startsWith("https://"))
362
- );
363
- }
364
-
365
- function readFileAsBuffer(filePath) {
366
- const buffer = fs.readFileSync(filePath);
367
- const ext = path.extname(filePath).replace(".", "").toLowerCase();
368
- const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
369
- const filename = path.basename(filePath);
370
- return { buffer, mime, filename };
371
- }
372
-
373
- function fetchRemoteLogo(url, redirectCount = 0) {
374
- return new Promise((resolve, reject) => {
375
- if (redirectCount > 5) return reject(new Error("Too many redirects"));
376
-
377
- const client = url.startsWith("https://") ? https : http;
378
-
379
- client
380
- .get(url, (res) => {
381
- if (
382
- [301, 302, 303, 307, 308].includes(res.statusCode) &&
383
- res.headers.location
384
- ) {
385
- return fetchRemoteLogo(res.headers.location, redirectCount + 1)
386
- .then(resolve)
387
- .catch(reject);
388
- }
389
-
390
- if (res.statusCode !== 200) {
391
- res.resume();
392
- return reject(new Error(`HTTP ${res.statusCode}`));
393
- }
394
-
395
- const contentType = res.headers["content-type"] || "";
396
- const mime = contentType.split(";")[0].trim();
397
-
398
- if (!mime.startsWith("image/")) {
399
- res.resume();
400
- return reject(
401
- new Error(
402
- `URL did not return an image (Content-Type: "${mime || "unknown"}"). Make sure the URL points directly to an image file.`,
403
- ),
404
- );
405
- }
406
-
407
- const urlPath = new URL(url).pathname;
408
- const ext = path.extname(urlPath) || inferExtFromMime(mime);
409
- const filename = path.basename(urlPath) || `logo${ext}`;
410
-
411
- const chunks = [];
412
- res.on("data", (chunk) => chunks.push(chunk));
413
- res.on("end", () =>
414
- resolve({ buffer: Buffer.concat(chunks), mime, filename }),
415
- );
416
- res.on("error", reject);
417
- })
418
- .on("error", reject);
419
- });
420
- }
421
-
422
- function inferExtFromMime(mime) {
423
- const map = {
424
- "image/png": ".png",
425
- "image/jpeg": ".jpg",
426
- "image/gif": ".gif",
427
- "image/svg+xml": ".svg",
428
- "image/webp": ".webp",
429
- };
430
- return map[mime] || ".png";
431
- }
432
-
433
- function generateCss(report, today, reportName, logoUrl) {
434
- return `.side-nav{background: #091628 !important; max-width:200px !important}.side-nav__brand{background:url('${logoUrl}') no-repeat center left !important;background-size:contain !important;height:80px;width:200px;display:flex !important;align-items:center;padding-left:80px}.side-nav__brand img,.side-nav__brand svg{display:none !important}.side-nav__brand-text{font-size:0 !important;display:block !important;padding: 0 8px;}.side-nav__brand-text::after{content:'${report.brandName}';font-size:23px;color:white;}.widget__title{font-weight:lighter;margin-bottom:15px;margin-top:0;text-transform:uppercase}.widget__flex-line:first-child .widget__title{font-size:0}.widget__flex-line:first-child .widget__title::before{content:'${reportName} ${today}';display:block;font-size:18px;font-weight:lighter;text-transform:uppercase}.widget__flex-line:first-child .widget__subtitle::after{content:' ${Intl.DateTimeFormat().resolvedOptions().timeZone}';font-size:14px;}.widget__flex-line:first-child .widget__subtitle{font-size:14px}.widget__flex-line:not(:first-child) .widget__title{font-size:inherit;font-weight:lighter}`;
435
- }
436
-
437
- function injectCssAndReturn(cssPath, dynamicCss) {
438
- let css = fs.readFileSync(cssPath, "utf8");
439
-
440
- const startIdx = css.indexOf(CSS_START_MARKER);
441
- const endIdx = css.indexOf(CSS_END_MARKER);
442
- if (startIdx !== -1 && endIdx !== -1) {
443
- css = css.slice(0, startIdx) + css.slice(endIdx + CSS_END_MARKER.length);
444
- }
445
-
446
- css = css.trimEnd();
447
- css += `\n${CSS_START_MARKER}\n${dynamicCss}\n${CSS_END_MARKER}\n`;
448
- return css;
449
- }
450
-
451
- function updateSingleFileHtml(
452
- htmlPath,
453
- report,
454
- reportName,
455
- faviconDataUrl,
456
- cssDataUrl,
457
- ) {
458
- let html = fs.readFileSync(htmlPath, "utf8");
459
-
460
- html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
461
-
462
- html = html.replace(
463
- /<link rel="icon" href="data:image\/[^"]+"/,
464
- `<link rel="icon" href="${faviconDataUrl}"`,
465
- );
466
-
467
- html = html.replace(
468
- /<link rel="stylesheet" type="text\/css" href="data:text\/css;base64,[^"]+"/,
469
- `<link rel="stylesheet" type="text/css" href="${cssDataUrl}"`,
470
- );
471
-
472
- fs.writeFileSync(htmlPath, html, "utf8");
473
- }
474
-
475
- function updateHtml(htmlPath, report, reportName, faviconDataUrl) {
476
- let html = fs.readFileSync(htmlPath, "utf8");
477
- html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
478
- html = html.replace(
479
- /<link rel="icon" href=".*?">/,
480
- `<link rel="icon" href="${faviconDataUrl}">`,
481
- );
482
- fs.writeFileSync(htmlPath, html, "utf8");
483
- }
484
-
485
- module.exports = { reportCustomizer };
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const https = require("https");
4
+ const http = require("http");
5
+
6
+ const CSS_START_MARKER = "/* ARTES_DYNAMIC_START */";
7
+ const CSS_END_MARKER = "/* ARTES_DYNAMIC_END */";
8
+
9
+ function reportCustomizer() {
10
+ delete require.cache[require.resolve("../../../cucumber.config.js")];
11
+ const cucumberConfig = require("../../../cucumber.config.js");
12
+
13
+ const report = cucumberConfig.report;
14
+ const today = new Date().toLocaleDateString("en-US", {
15
+ month: "numeric",
16
+ day: "numeric",
17
+ year: "numeric",
18
+ });
19
+ const reportName =
20
+ typeof report.reportName === "string"
21
+ ? report.reportName
22
+ : report.reportName.name || "";
23
+
24
+ const { buffer: faviconBuffer, mime: faviconMime } =
25
+ readFileAsBuffer(defaultLogoPath());
26
+ const faviconDataUrl = `data:${faviconMime};base64,${faviconBuffer.toString("base64")}`;
27
+
28
+ if (isRemoteUrl(report.logo)) {
29
+ return fetchRemoteLogo(report.logo)
30
+ .catch((err) => {
31
+ console.warn(
32
+ `[artes] Warning: failed to fetch logo from "${report.logo}": ${err.message}. Falling back to default logo.`,
33
+ );
34
+ return readFileAsBuffer(defaultLogoPath());
35
+ })
36
+ .then(({ buffer, mime, filename }) => {
37
+ applyLogo(
38
+ cucumberConfig,
39
+ report,
40
+ today,
41
+ reportName,
42
+ buffer,
43
+ mime,
44
+ filename,
45
+ faviconDataUrl,
46
+ );
47
+ });
48
+ }
49
+
50
+ const logoSrc = resolveLogoPath(report.logo);
51
+ const logoExt = path.extname(logoSrc).replace(".", "").toLowerCase();
52
+ const logoMime = logoExt === "svg" ? "image/svg+xml" : `image/${logoExt}`;
53
+ const logoBuffer = fs.readFileSync(logoSrc);
54
+ applyLogo(
55
+ cucumberConfig,
56
+ report,
57
+ today,
58
+ reportName,
59
+ logoBuffer,
60
+ logoMime,
61
+ path.basename(logoSrc),
62
+ faviconDataUrl,
63
+ );
64
+ }
65
+
66
+ function applyLogo(
67
+ cucumberConfig,
68
+ report,
69
+ today,
70
+ reportName,
71
+ logoBuffer,
72
+ logoMime,
73
+ logoFilename,
74
+ faviconDataUrl,
75
+ ) {
76
+ const logoBase64 = logoBuffer.toString("base64");
77
+ const logoDataUrl = `data:${logoMime};base64,${logoBase64}`;
78
+
79
+ const testPercentage = cucumberConfig.default?.testPercentage ?? 0;
80
+ let testCoverageWidgetCss = "";
81
+
82
+ if (testPercentage > 0) {
83
+ const { testCoverageCalculator } = require("./testCoverageCalculator");
84
+ const testCoverage = testCoverageCalculator({ silent: true });
85
+
86
+ if (testCoverage) {
87
+ const meetsThreshold = testCoverage.percentage >= testPercentage;
88
+
89
+ testCoverageWidgetCss = generateTestCoverageWidgetCss(
90
+ testCoverage,
91
+ testPercentage,
92
+ meetsThreshold,
93
+ );
94
+ }
95
+ }
96
+
97
+ if (cucumberConfig.report.singleFileReport) {
98
+ const htmlPath = path.resolve(
99
+ __dirname,
100
+ "../../../../../report/index.html",
101
+ );
102
+ const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
103
+
104
+ const dynamicCss =
105
+ generateCss(report, today, reportName, logoDataUrl) +
106
+ (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
107
+ const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
108
+ const cssBase64 = Buffer.from(modifiedCss).toString("base64");
109
+ const cssDataUrl = `data:text/css;base64,${cssBase64}`;
110
+
111
+ updateSingleFileHtml(
112
+ htmlPath,
113
+ report,
114
+ reportName,
115
+ faviconDataUrl,
116
+ cssDataUrl,
117
+ );
118
+ } else {
119
+ const htmlPath = path.resolve(
120
+ __dirname,
121
+ "../../../../../report/index.html",
122
+ );
123
+ const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
124
+ const reportDir = path.resolve(__dirname, "../../../../../report");
125
+ const reportCssPath = path.join(reportDir, "styles.css");
126
+
127
+ const logoDest = path.join(reportDir, logoFilename);
128
+ fs.writeFileSync(logoDest, logoBuffer);
129
+
130
+ const dynamicCss =
131
+ generateCss(report, today, reportName, logoFilename) +
132
+ (testCoverageWidgetCss ? "\n" + testCoverageWidgetCss : "");
133
+ const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
134
+ fs.writeFileSync(reportCssPath, modifiedCss, "utf8");
135
+
136
+ updateHtml(htmlPath, report, reportName, faviconDataUrl);
137
+ }
138
+ }
139
+
140
+ function generateTestCoverageWidgetCss(
141
+ testCoverage,
142
+ testPercentage,
143
+ meetsThreshold,
144
+ ) {
145
+ const fill = Math.min(testCoverage.percentage, 100);
146
+ const fillPct = fill.toFixed(4);
147
+ const pctLabel = testCoverage.percentage.toFixed(2);
148
+ const statusColor = meetsThreshold ? "#4caf50" : "#f44336";
149
+ const statusBg = meetsThreshold
150
+ ? "rgba(76,175,80,.13)"
151
+ : "rgba(244,67,54,.13)";
152
+ const statusVerb = meetsThreshold ? "passed" : "failed";
153
+ const subtitleTxt = `${testCoverage.passed} / ${testCoverage.totalTests} passed`;
154
+ const statusLine = `Tests ${statusVerb} \u2014 required ${testPercentage}% with ${pctLabel}%`;
155
+
156
+ const barGradient = `linear-gradient(to right, #f44336 0%, #ff9800 35%, #ffeb3b 60%, #4caf50 100%)`;
157
+
158
+ const svgLabels = [
159
+ { val: "0", x: "0%", anchor: "start" },
160
+ { val: "20", x: "20%", anchor: "middle" },
161
+ { val: "40", x: "40%", anchor: "middle" },
162
+ { val: "60", x: "60%", anchor: "middle" },
163
+ { val: "80", x: "80%", anchor: "middle" },
164
+ { val: "100", x: "100%", anchor: "end" },
165
+ ];
166
+
167
+ const labelNodes = svgLabels
168
+ .map(
169
+ (l) =>
170
+ `<text x="${l.x}" y="10" text-anchor="${l.anchor}" font-family="sans-serif" font-size="10" fill="#bbb">${l.val}</text>`,
171
+ )
172
+ .join("");
173
+
174
+ const pointerColor = meetsThreshold ? "#4caf50" : "#f44336";
175
+ const px = (fill * 10).toFixed(1);
176
+
177
+ const labelPointerSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="18">
178
+ <text x="${fillPct}%" y="14" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="${pointerColor}">${pctLabel}%</text>
179
+ </svg>`;
180
+
181
+ const stemSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 18" preserveAspectRatio="none" width="100%" height="18">
182
+ <line x1="${px}" y1="6" x2="${px}" y2="10" stroke="${pointerColor}" stroke-width="6"/>
183
+ <polygon points="${parseFloat(px) - 12},8 ${parseFloat(px) + 12},8 ${parseFloat(px)},18" fill="${pointerColor}"/>
184
+ </svg>`;
185
+
186
+ const labelPointerB64 = Buffer.from(labelPointerSvg).toString("base64");
187
+ const labelPointerDataUrl = `data:image/svg+xml;base64,${labelPointerB64}`;
188
+ const stemB64 = Buffer.from(stemSvg).toString("base64");
189
+ const stemDataUrl = `data:image/svg+xml;base64,${stemB64}`;
190
+
191
+ const pointerDataUrl = stemDataUrl;
192
+ const pointerLabelDataUrl = labelPointerDataUrl;
193
+
194
+ const labelSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="14">${labelNodes}</svg>`;
195
+
196
+ const svgB64 = Buffer.from(labelSvg).toString("base64");
197
+ const svgDataUrl = `data:image/svg+xml;base64,${svgB64}`;
198
+
199
+ const CARD_H = 180;
200
+ const TITLE_PAD = 14;
201
+ const POINTER_FROM_CARD = 62;
202
+ const BAR_FROM_CARD = 98;
203
+ const LABEL_FROM_CARD = 116;
204
+ const STATUS_FROM_CARD = 138;
205
+
206
+ return `
207
+ /* ── ARTES TEST COVERAGE WIDGET ─────────────────────────────────────────── */
208
+
209
+ [data-id="summary"] {
210
+ margin-bottom: ${CARD_H + 22}px !important;
211
+ position: relative;
212
+ }
213
+
214
+ /* 1. Card shell + "TEST COVERAGE" title — 21px black */
215
+ [data-id="summary"]::after {
216
+ content: 'TEST COVERAGE';
217
+ position: absolute;
218
+ top: calc(100% + 10px);
219
+ left: 0;
220
+ right: 0;
221
+ height: ${CARD_H}px;
222
+ background: #fff;
223
+ border-radius: 3px;
224
+ box-shadow: 0 1px 3px rgba(0,0,0,.15);
225
+ padding: ${TITLE_PAD}px 16px 0;
226
+ box-sizing: border-box;
227
+ font-size: 21px;
228
+ font-weight: 100;
229
+ color: #000;
230
+ text-transform: uppercase;
231
+ line-height: 1.4;
232
+ pointer-events: none;
233
+ z-index: 1;
234
+ }
235
+
236
+ /* 2. "1 / 1 passed 100.00%" — subtitle + percentage on same line */
237
+ [data-id="summary"] .widget__body > div {
238
+ position: relative;
239
+ }
240
+
241
+ [data-id="summary"] .widget__body > div::before {
242
+ content: '${subtitleTxt} ${pctLabel}%';
243
+ position: absolute;
244
+ top: calc(100% + 10px + 46px);
245
+ left: 0;
246
+ font-size: 14px;
247
+ font-weight: 400;
248
+ color: #999;
249
+ line-height: 1;
250
+ letter-spacing: 0;
251
+ pointer-events: none;
252
+ z-index: 2;
253
+ }
254
+
255
+ /* 3. Pointer + threshold marker layered in one element */
256
+ [data-id="summary"] .widget__body > div > *:first-child {
257
+ position: relative;
258
+ }
259
+
260
+ [data-id="summary"] .widget__body > div > *:first-child::after {
261
+ content: '';
262
+ position: absolute;
263
+ top: calc(100% + 10px + ${POINTER_FROM_CARD}px);
264
+ left: 16px;
265
+ right: 16px;
266
+ height: 36px;
267
+ background-image: url("${pointerLabelDataUrl}"), url("${pointerDataUrl}");
268
+ background-repeat: no-repeat;
269
+ background-size: 100% 50%, 100% 50%;
270
+ background-position: top, bottom;
271
+ pointer-events: none;
272
+ z-index: 3;
273
+ }
274
+
275
+ /* 4. SVG label row — perfectly aligned under bar via background-image */
276
+ [data-id="summary"] .widget__body > div::after {
277
+ content: '';
278
+ position: absolute;
279
+ top: calc(100% + 10px + ${LABEL_FROM_CARD}px);
280
+ left: 16px;
281
+ right: 16px;
282
+ height: 14px;
283
+ background-image: url("${svgDataUrl}");
284
+ background-repeat: no-repeat;
285
+ background-size: 100% 100%;
286
+ pointer-events: none;
287
+ z-index: 2;
288
+ }
289
+
290
+ /* 5. Gradient bar */
291
+ [data-id="summary"] .widget__body {
292
+ position: static;
293
+ }
294
+
295
+ [data-id="summary"] .widget__body::before {
296
+ content: '';
297
+ position: absolute;
298
+ top: calc(100% + 10px + ${BAR_FROM_CARD}px);
299
+ left: 16px;
300
+ right: 16px;
301
+ height: 14px;
302
+ border-radius: 7px;
303
+ background: ${barGradient};
304
+ box-shadow: inset 0 1px 3px rgba(0,0,0,.12);
305
+ pointer-events: none;
306
+ z-index: 2;
307
+ }
308
+
309
+ /* 5. Status pill */
310
+ [data-id="summary"] .widget__body::after {
311
+ content: '${statusLine}';
312
+ position: absolute;
313
+ top: calc(100% + 10px + ${STATUS_FROM_CARD}px);
314
+ left: 16px;
315
+ right: 16px;
316
+ font-size: 12px;
317
+ font-weight: 500;
318
+ color: ${statusColor};
319
+ background: ${statusBg};
320
+ padding: 5px 10px;
321
+ border-radius: 3px;
322
+ box-sizing: border-box;
323
+ pointer-events: none;
324
+ z-index: 2;
325
+ }
326
+
327
+ /* ── End ARTES TEST COVERAGE WIDGET ─────────────────────────────────────── */
328
+ `;
329
+ }
330
+
331
+ function resolveLogoPath(logoConfig) {
332
+ if (!logoConfig) {
333
+ return defaultLogoPath();
334
+ }
335
+
336
+ const resolved = path.isAbsolute(logoConfig)
337
+ ? logoConfig
338
+ : path.resolve(process.cwd(), logoConfig);
339
+
340
+ if (!fs.existsSync(resolved)) {
341
+ // console.warn(`[artes] Warning: logo not found at "${resolved}". Falling back to default logo.`);
342
+ return defaultLogoPath();
343
+ }
344
+
345
+ return resolved;
346
+ }
347
+
348
+ function defaultLogoPath() {
349
+ return path.resolve(
350
+ process.cwd(),
351
+ "node_modules",
352
+ "artes",
353
+ "assets",
354
+ "logo.png",
355
+ );
356
+ }
357
+
358
+ function isRemoteUrl(logoConfig) {
359
+ return (
360
+ typeof logoConfig === "string" &&
361
+ (logoConfig.startsWith("http://") || logoConfig.startsWith("https://"))
362
+ );
363
+ }
364
+
365
+ function readFileAsBuffer(filePath) {
366
+ const buffer = fs.readFileSync(filePath);
367
+ const ext = path.extname(filePath).replace(".", "").toLowerCase();
368
+ const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
369
+ const filename = path.basename(filePath);
370
+ return { buffer, mime, filename };
371
+ }
372
+
373
+ function fetchRemoteLogo(url, redirectCount = 0) {
374
+ return new Promise((resolve, reject) => {
375
+ if (redirectCount > 5) return reject(new Error("Too many redirects"));
376
+
377
+ const client = url.startsWith("https://") ? https : http;
378
+
379
+ client
380
+ .get(url, (res) => {
381
+ if (
382
+ [301, 302, 303, 307, 308].includes(res.statusCode) &&
383
+ res.headers.location
384
+ ) {
385
+ return fetchRemoteLogo(res.headers.location, redirectCount + 1)
386
+ .then(resolve)
387
+ .catch(reject);
388
+ }
389
+
390
+ if (res.statusCode !== 200) {
391
+ res.resume();
392
+ return reject(new Error(`HTTP ${res.statusCode}`));
393
+ }
394
+
395
+ const contentType = res.headers["content-type"] || "";
396
+ const mime = contentType.split(";")[0].trim();
397
+
398
+ if (!mime.startsWith("image/")) {
399
+ res.resume();
400
+ return reject(
401
+ new Error(
402
+ `URL did not return an image (Content-Type: "${mime || "unknown"}"). Make sure the URL points directly to an image file.`,
403
+ ),
404
+ );
405
+ }
406
+
407
+ const urlPath = new URL(url).pathname;
408
+ const ext = path.extname(urlPath) || inferExtFromMime(mime);
409
+ const filename = path.basename(urlPath) || `logo${ext}`;
410
+
411
+ const chunks = [];
412
+ res.on("data", (chunk) => chunks.push(chunk));
413
+ res.on("end", () =>
414
+ resolve({ buffer: Buffer.concat(chunks), mime, filename }),
415
+ );
416
+ res.on("error", reject);
417
+ })
418
+ .on("error", reject);
419
+ });
420
+ }
421
+
422
+ function inferExtFromMime(mime) {
423
+ const map = {
424
+ "image/png": ".png",
425
+ "image/jpeg": ".jpg",
426
+ "image/gif": ".gif",
427
+ "image/svg+xml": ".svg",
428
+ "image/webp": ".webp",
429
+ };
430
+ return map[mime] || ".png";
431
+ }
432
+
433
+ function generateCss(report, today, reportName, logoUrl) {
434
+ return `.side-nav{background: #091628 !important; max-width:200px !important}.side-nav__brand{background:url('${logoUrl}') no-repeat center left !important;background-size:contain !important;height:80px;width:200px;display:flex !important;align-items:center;padding-left:80px}.side-nav__brand img,.side-nav__brand svg{display:none !important}.side-nav__brand-text{font-size:0 !important;display:block !important;padding: 0 8px;}.side-nav__brand-text::after{content:'${report.brandName}';font-size:23px;color:white;}.widget__title{font-weight:lighter;margin-bottom:15px;margin-top:0;text-transform:uppercase}.widget__flex-line:first-child .widget__title{font-size:0}.widget__flex-line:first-child .widget__title::before{content:'${reportName} ${today}';display:block;font-size:18px;font-weight:lighter;text-transform:uppercase}.widget__flex-line:first-child .widget__subtitle::after{content:' ${Intl.DateTimeFormat().resolvedOptions().timeZone}';font-size:14px;}.widget__flex-line:first-child .widget__subtitle{font-size:14px}.widget__flex-line:not(:first-child) .widget__title{font-size:inherit;font-weight:lighter}`;
435
+ }
436
+
437
+ function injectCssAndReturn(cssPath, dynamicCss) {
438
+ let css = fs.readFileSync(cssPath, "utf8");
439
+
440
+ const startIdx = css.indexOf(CSS_START_MARKER);
441
+ const endIdx = css.indexOf(CSS_END_MARKER);
442
+ if (startIdx !== -1 && endIdx !== -1) {
443
+ css = css.slice(0, startIdx) + css.slice(endIdx + CSS_END_MARKER.length);
444
+ }
445
+
446
+ css = css.trimEnd();
447
+ css += `\n${CSS_START_MARKER}\n${dynamicCss}\n${CSS_END_MARKER}\n`;
448
+ return css;
449
+ }
450
+
451
+ function updateSingleFileHtml(
452
+ htmlPath,
453
+ report,
454
+ reportName,
455
+ faviconDataUrl,
456
+ cssDataUrl,
457
+ ) {
458
+ let html = fs.readFileSync(htmlPath, "utf8");
459
+
460
+ html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
461
+
462
+ html = html.replace(
463
+ /<link rel="icon" href="data:image\/[^"]+"/,
464
+ `<link rel="icon" href="${faviconDataUrl}"`,
465
+ );
466
+
467
+ html = html.replace(
468
+ /<link rel="stylesheet" type="text\/css" href="data:text\/css;base64,[^"]+"/,
469
+ `<link rel="stylesheet" type="text/css" href="${cssDataUrl}"`,
470
+ );
471
+
472
+ fs.writeFileSync(htmlPath, html, "utf8");
473
+ }
474
+
475
+ function updateHtml(htmlPath, report, reportName, faviconDataUrl) {
476
+ let html = fs.readFileSync(htmlPath, "utf8");
477
+ html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
478
+ html = html.replace(
479
+ /<link rel="icon" href=".*?">/,
480
+ `<link rel="icon" href="${faviconDataUrl}">`,
481
+ );
482
+ fs.writeFileSync(htmlPath, html, "utf8");
483
+ }
484
+
485
+ module.exports = { reportCustomizer };