artes 1.4.13 → 1.5.1
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/README.md +14 -1
- package/assets/logo.png +0 -0
- package/assets/styles.css +5 -0
- package/cucumber.config.js +9 -0
- package/docs/ciExecutors.md +198 -0
- package/executer.js +12 -0
- package/package.json +4 -2
- package/src/helper/controller/reportCustomizer.js +193 -0
- package/src/helper/executers/helper.js +9 -0
- package/src/helper/executers/projectCreator.js +3 -0
- package/src/helper/executers/reportGenerator.js +12 -0
- package/src/hooks/hooks.js +26 -1
package/cucumber.config.js
CHANGED
|
@@ -172,6 +172,15 @@ module.exports = {
|
|
|
172
172
|
worldParameters: artesConfig.worldParameters || {}, // Custom world parameters
|
|
173
173
|
},
|
|
174
174
|
report: {
|
|
175
|
+
logo: process.env.LOGO
|
|
176
|
+
? process.env.LOGO
|
|
177
|
+
: artesConfig?.logo || "./logo.png",
|
|
178
|
+
brandName: process.env.BRAND_NAME
|
|
179
|
+
? process.env.BRAND_NAME
|
|
180
|
+
: artesConfig?.brandName || "ARTES",
|
|
181
|
+
reportName: process.env.REPORT_NAME
|
|
182
|
+
? process.env.REPORT_NAME
|
|
183
|
+
: artesConfig?.reportName || "ARTES REPORT",
|
|
175
184
|
singleFileReport:
|
|
176
185
|
process.env.SINGLE_FILE_REPORT == "true"
|
|
177
186
|
? true
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# CI/CD Executor Integration guide
|
|
2
|
+
|
|
3
|
+
This document describes the environment variables used to identify which CI/CD runner is executing your tests. When these variables are detected, the corresponding runner will automatically appear in your report.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
The reporter inspects environment variables at runtime to detect the current CI/CD platform. **No configuration is needed** — simply ensure the variables below are present in your environment, and the correct runner will be shown in the report automatically.
|
|
10
|
+
|
|
11
|
+
### ✅ Supported Platforms
|
|
12
|
+
|
|
13
|
+
| Platform | Auto-detected via |
|
|
14
|
+
| --------------------- | ---------------------------------- |
|
|
15
|
+
| **GitHub Actions** | `GITHUB_RUN_ID` |
|
|
16
|
+
| **Jenkins** | `JENKINS_HOME` |
|
|
17
|
+
| **GitLab CI** | `CI_PIPELINE_ID` |
|
|
18
|
+
| **Bitbucket Pipelines** | `BITBUCKET_BUILD_NUMBER` |
|
|
19
|
+
| **CircleCI** | `CIRCLE_WORKFLOW_ID` |
|
|
20
|
+
| **Azure Pipelines** | `BUILD_BUILDID` |
|
|
21
|
+
| **TeamCity** | `BUILD_NUMBER` + `TEAMCITY_VERSION`|
|
|
22
|
+
| **Travis CI** | `TRAVIS_BUILD_NUMBER` |
|
|
23
|
+
| **Bamboo** | `bamboo_buildNumber` |
|
|
24
|
+
| **Local / Other** | Fallback — shown as "Local Run" |
|
|
25
|
+
|
|
26
|
+
When none of the above variables are present, the executor is shown as **Manual Execution** in the report.
|
|
27
|
+
|
|
28
|
+
### Quick Reference — All Variables
|
|
29
|
+
|
|
30
|
+
| Platform | Required (Detection) | Optional (Enrichment) |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| GitHub Actions | `GITHUB_RUN_ID` | `GITHUB_RUN_NUMBER`, `GITHUB_SERVER_URL`, `GITHUB_REPOSITORY` |
|
|
33
|
+
| Jenkins | `JENKINS_HOME` | `JOB_NAME`, `BUILD_NUMBER`, `BUILD_URL` |
|
|
34
|
+
| GitLab CI | `CI_PIPELINE_ID` | `CI_PIPELINE_IID`, `CI_PIPELINE_URL` |
|
|
35
|
+
| Bitbucket | `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BUILD_URL` |
|
|
36
|
+
| CircleCI | `CIRCLE_WORKFLOW_ID` | `CIRCLE_BUILD_NUM`, `CIRCLE_BUILD_URL` |
|
|
37
|
+
| Azure Pipelines | `BUILD_BUILDID` | `BUILD_BUILDURI` |
|
|
38
|
+
| TeamCity | `BUILD_NUMBER` + `TEAMCITY_VERSION` | `BUILD_URL` |
|
|
39
|
+
| Travis CI | `TRAVIS_BUILD_NUMBER` | `TRAVIS_BUILD_WEB_URL` |
|
|
40
|
+
| Bamboo | `bamboo_buildNumber` | `bamboo_resultsUrl` |
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Supported CI/CD Platforms
|
|
44
|
+
|
|
45
|
+
### ✅ GitHub Actions
|
|
46
|
+
|
|
47
|
+
Automatically detected when running inside a GitHub Actions workflow.
|
|
48
|
+
|
|
49
|
+
| Variable | Description | Example |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `GITHUB_RUN_ID` | Unique ID of the workflow run *(required for detection)* | `9876543210` |
|
|
52
|
+
| `GITHUB_RUN_NUMBER` | Sequential run number for the workflow | `42` |
|
|
53
|
+
| `GITHUB_SERVER_URL` | Base URL of the GitHub server | `https://github.com` |
|
|
54
|
+
| `GITHUB_REPOSITORY` | Owner and repository name | `my-org/my-repo` |
|
|
55
|
+
|
|
56
|
+
> These variables are set automatically by GitHub Actions. No manual setup needed.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### ✅ Jenkins
|
|
61
|
+
|
|
62
|
+
Detected when `JENKINS_HOME` is set in the environment.
|
|
63
|
+
|
|
64
|
+
| Variable | Description | Example |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `JENKINS_HOME` | Jenkins home directory path *(required for detection)* | `/var/jenkins_home` |
|
|
67
|
+
| `JOB_NAME` | Name of the Jenkins job | `my-pipeline` |
|
|
68
|
+
| `BUILD_NUMBER` | Sequential build number | `101` |
|
|
69
|
+
| `BUILD_URL` | Full URL to the build page | `http://jenkins.example.com/job/my-pipeline/101/` |
|
|
70
|
+
|
|
71
|
+
> These variables are set automatically by Jenkins. No manual setup needed.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### ✅ GitLab CI
|
|
76
|
+
|
|
77
|
+
Detected when `CI_PIPELINE_ID` is set in the environment.
|
|
78
|
+
|
|
79
|
+
| Variable | Description | Example |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `CI_PIPELINE_ID` | Unique pipeline ID *(required for detection)* | `123456789` |
|
|
82
|
+
| `CI_PIPELINE_IID` | Project-scoped pipeline number (used for display) | `55` |
|
|
83
|
+
| `CI_PIPELINE_URL` | Full URL to the pipeline | `https://gitlab.com/my-org/my-repo/-/pipelines/55` |
|
|
84
|
+
|
|
85
|
+
> These variables are set automatically by GitLab CI/CD. No manual setup needed.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### ✅ Bitbucket Pipelines
|
|
90
|
+
|
|
91
|
+
Detected when `BITBUCKET_BUILD_NUMBER` is set in the environment.
|
|
92
|
+
|
|
93
|
+
| Variable | Description | Example |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `BITBUCKET_BUILD_NUMBER` | Unique build number *(required for detection)* | `77` |
|
|
96
|
+
| `BITBUCKET_BUILD_URL` | URL to the Bitbucket pipeline build | `https://bitbucket.org/my-org/my-repo/addon/pipelines/home#!/results/77` |
|
|
97
|
+
|
|
98
|
+
> These variables are set automatically by Bitbucket Pipelines. No manual setup needed.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### ✅ CircleCI
|
|
103
|
+
|
|
104
|
+
Detected when `CIRCLE_WORKFLOW_ID` is set in the environment.
|
|
105
|
+
|
|
106
|
+
| Variable | Description | Example |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `CIRCLE_WORKFLOW_ID` | Unique workflow ID *(required for detection)* | `abc-123-def-456` |
|
|
109
|
+
| `CIRCLE_BUILD_NUM` | Sequential build number | `88` |
|
|
110
|
+
| `CIRCLE_BUILD_URL` | URL to the CircleCI build | `https://circleci.com/gh/my-org/my-repo/88` |
|
|
111
|
+
|
|
112
|
+
> These variables are set automatically by CircleCI. No manual setup needed.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### ✅ Azure Pipelines
|
|
117
|
+
|
|
118
|
+
Detected when `BUILD_BUILDID` is set in the environment.
|
|
119
|
+
|
|
120
|
+
| Variable | Description | Example |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `BUILD_BUILDID` | Unique build ID *(required for detection)* | `5001` |
|
|
123
|
+
| `BUILD_BUILDURI` | URI to the Azure DevOps build | `vstfs:///Build/Build/5001` |
|
|
124
|
+
|
|
125
|
+
> These variables are set automatically by Azure Pipelines. No manual setup needed.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### ✅ TeamCity
|
|
130
|
+
|
|
131
|
+
Detected when **both** `BUILD_NUMBER` and `TEAMCITY_VERSION` are set.
|
|
132
|
+
|
|
133
|
+
| Variable | Description | Example |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `TEAMCITY_VERSION` | TeamCity server version *(required for detection)* | `2023.11` |
|
|
136
|
+
| `BUILD_NUMBER` | Sequential build number *(required for detection)* | `99` |
|
|
137
|
+
| `BUILD_URL` | URL to the TeamCity build | `http://teamcity.example.com/build/99` |
|
|
138
|
+
|
|
139
|
+
> These variables are set automatically by TeamCity. No manual setup needed.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### ✅ Travis CI
|
|
144
|
+
|
|
145
|
+
Detected when `TRAVIS_BUILD_NUMBER` is set in the environment.
|
|
146
|
+
|
|
147
|
+
| Variable | Description | Example |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `TRAVIS_BUILD_NUMBER` | Sequential build number *(required for detection)* | `123` |
|
|
150
|
+
| `TRAVIS_BUILD_WEB_URL` | URL to the Travis CI build | `https://travis-ci.com/my-org/my-repo/builds/123` |
|
|
151
|
+
|
|
152
|
+
> These variables are set automatically by Travis CI. No manual setup needed.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### ✅ Bamboo
|
|
157
|
+
|
|
158
|
+
Detected when `bamboo_buildNumber` is set in the environment.
|
|
159
|
+
|
|
160
|
+
| Variable | Description | Example |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `bamboo_buildNumber` | Sequential build number *(required for detection)* | `56` |
|
|
163
|
+
| `bamboo_resultsUrl` | URL to the Bamboo build results | `http://bamboo.example.com/browse/MY-PROJECT-56` |
|
|
164
|
+
|
|
165
|
+
> These variables are set automatically by Bamboo. No manual setup needed.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### 🖥️ Local / Manual Run
|
|
170
|
+
|
|
171
|
+
If **none** of the above variables are detected, the report will show **"Local Run"** with build name `"Manual Execution"`. This is the default fallback for local development.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Running in a Custom or Self-Hosted Environment
|
|
176
|
+
|
|
177
|
+
If you're using a custom CI system or a Docker container and want a specific runner to appear in the report, manually export the required variables before running your tests.
|
|
178
|
+
|
|
179
|
+
**Example — simulating a Jenkins build in a Docker container:**
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
docker run \
|
|
183
|
+
-e JENKINS_HOME=/var/jenkins_home \
|
|
184
|
+
-e JOB_NAME="my-test-suite" \
|
|
185
|
+
-e BUILD_NUMBER=42 \
|
|
186
|
+
-e BUILD_URL="http://jenkins.example.com/job/my-test-suite/42/" \
|
|
187
|
+
my-test-image npm test
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Example — simulating a GitHub Actions build locally:**
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
export GITHUB_RUN_ID=9999999999
|
|
194
|
+
export GITHUB_RUN_NUMBER=1
|
|
195
|
+
export GITHUB_SERVER_URL=https://github.com
|
|
196
|
+
export GITHUB_REPOSITORY=my-org/my-repo
|
|
197
|
+
|
|
198
|
+
```
|
package/executer.js
CHANGED
|
@@ -37,6 +37,9 @@ const flags = {
|
|
|
37
37
|
trace: args.includes("-t") || args.includes("--trace"),
|
|
38
38
|
reportWithTrace: args.includes("-rwt") || args.includes("--reportWithTrace"),
|
|
39
39
|
singleFileReport: args.includes("--singleFileReport"),
|
|
40
|
+
customLogo: args.includes("--logo"),
|
|
41
|
+
customBrandName:args.includes("--brandName"),
|
|
42
|
+
customReportName:args.includes("--reportName"),
|
|
40
43
|
zip: args.includes("--zip"),
|
|
41
44
|
features: args.includes("--features"),
|
|
42
45
|
stepDef: args.includes("--stepDef"),
|
|
@@ -63,6 +66,9 @@ const flags = {
|
|
|
63
66
|
|
|
64
67
|
const env = getArgValue("--env");
|
|
65
68
|
const vars = getArgValue("--saveVar");
|
|
69
|
+
const logo = getArgValue("--logo");
|
|
70
|
+
const brandName = getArgValue("--brandName");
|
|
71
|
+
const reportName = getArgValue("--reportName");
|
|
66
72
|
const featureFiles = getArgValue("--features");
|
|
67
73
|
const features = flags.features && featureFiles;
|
|
68
74
|
const stepDef = getArgValue("--stepDef");
|
|
@@ -93,6 +99,10 @@ artesConfig.report
|
|
|
93
99
|
]))
|
|
94
100
|
: "";
|
|
95
101
|
|
|
102
|
+
flags.customLogo ? (process.env.LOGO = logo) : "";
|
|
103
|
+
flags.customBrandName ? (process.env.BRAND_NAME = brandName) : "";
|
|
104
|
+
flags.customReportName ? (process.env.REPORT_NAME = reportName) : "";
|
|
105
|
+
|
|
96
106
|
flags.reportSuccess ? (process.env.REPORT_SUCCESS = true) : "";
|
|
97
107
|
|
|
98
108
|
flags.tags && console.log("Running tags:", tags);
|
|
@@ -443,10 +453,12 @@ if (fs.existsSync(source)) {
|
|
|
443
453
|
){
|
|
444
454
|
const executor = getExecutor();
|
|
445
455
|
|
|
456
|
+
if(fs.existsSync(path.join(process.cwd(), "node_modules", "artes",'allure-result'))){
|
|
446
457
|
fs.writeFileSync(
|
|
447
458
|
path.join(process.cwd(), "node_modules", "artes",'allure-result',"executor.json"),
|
|
448
459
|
JSON.stringify(executor, null, 2)
|
|
449
460
|
);
|
|
461
|
+
}
|
|
450
462
|
|
|
451
463
|
generateReport();
|
|
452
464
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "The simplest way to automate UI and API tests using Cucumber-style steps.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"archiver": "^7.0.1",
|
|
29
29
|
"dayjs": "1.11.13",
|
|
30
30
|
"deasync": "^0.1.31",
|
|
31
|
-
"playwright": "^1.58.2"
|
|
31
|
+
"playwright": "^1.58.2",
|
|
32
|
+
"ffmpeg-static": "^5.3.0",
|
|
33
|
+
"ffprobe-static": "^3.1.0"
|
|
32
34
|
},
|
|
33
35
|
"repository": {
|
|
34
36
|
"type": "git",
|
|
@@ -0,0 +1,193 @@
|
|
|
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", { month: "numeric", day: "numeric", year: "numeric" });
|
|
15
|
+
const reportName = typeof report.reportName === "string" ? report.reportName : report.reportName.name || "";
|
|
16
|
+
|
|
17
|
+
const { buffer: faviconBuffer, mime: faviconMime } = readFileAsBuffer(defaultLogoPath());
|
|
18
|
+
const faviconDataUrl = `data:${faviconMime};base64,${faviconBuffer.toString("base64")}`;
|
|
19
|
+
|
|
20
|
+
if (isRemoteUrl(report.logo)) {
|
|
21
|
+
return fetchRemoteLogo(report.logo)
|
|
22
|
+
.catch((err) => {
|
|
23
|
+
console.warn(`[artes] Warning: failed to fetch logo from "${report.logo}": ${err.message}. Falling back to default logo.`);
|
|
24
|
+
return readFileAsBuffer(defaultLogoPath());
|
|
25
|
+
})
|
|
26
|
+
.then(({ buffer, mime, filename }) => {
|
|
27
|
+
applyLogo(cucumberConfig, report, today, reportName, buffer, mime, filename, faviconDataUrl);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const logoSrc = resolveLogoPath(report.logo);
|
|
32
|
+
const logoExt = path.extname(logoSrc).replace(".", "").toLowerCase();
|
|
33
|
+
const logoMime = logoExt === "svg" ? "image/svg+xml" : `image/${logoExt}`;
|
|
34
|
+
const logoBuffer = fs.readFileSync(logoSrc);
|
|
35
|
+
applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMime, path.basename(logoSrc), faviconDataUrl);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyLogo(cucumberConfig, report, today, reportName, logoBuffer, logoMime, logoFilename, faviconDataUrl) {
|
|
39
|
+
const logoBase64 = logoBuffer.toString("base64");
|
|
40
|
+
const logoDataUrl = `data:${logoMime};base64,${logoBase64}`;
|
|
41
|
+
|
|
42
|
+
if (cucumberConfig.report.singleFileReport) {
|
|
43
|
+
const htmlPath = path.resolve(__dirname, "../../../../../report/index.html");
|
|
44
|
+
const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
|
|
45
|
+
|
|
46
|
+
const dynamicCss = generateCss(report, today, reportName, logoDataUrl);
|
|
47
|
+
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
48
|
+
const cssBase64 = Buffer.from(modifiedCss).toString("base64");
|
|
49
|
+
const cssDataUrl = `data:text/css;base64,${cssBase64}`;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssDataUrl);
|
|
53
|
+
|
|
54
|
+
} else {
|
|
55
|
+
const htmlPath = path.resolve(__dirname, "../../../../../report/index.html");
|
|
56
|
+
const srcCssPath = path.resolve(__dirname, "../../../assets/styles.css");
|
|
57
|
+
const reportDir = path.resolve(__dirname, "../../../../../report");
|
|
58
|
+
const reportCssPath = path.join(reportDir, "styles.css");
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const logoDest = path.join(reportDir, logoFilename);
|
|
62
|
+
fs.writeFileSync(logoDest, logoBuffer);
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
const dynamicCss = generateCss(report, today, reportName, logoFilename);
|
|
66
|
+
|
|
67
|
+
const modifiedCss = injectCssAndReturn(srcCssPath, dynamicCss);
|
|
68
|
+
fs.writeFileSync(reportCssPath, modifiedCss, "utf8");
|
|
69
|
+
|
|
70
|
+
updateHtml(htmlPath, report, reportName, faviconDataUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveLogoPath(logoConfig) {
|
|
75
|
+
if (!logoConfig) {
|
|
76
|
+
return defaultLogoPath();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const resolved = path.isAbsolute(logoConfig)
|
|
80
|
+
? logoConfig
|
|
81
|
+
: path.resolve(process.cwd(), logoConfig);
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(resolved)) {
|
|
84
|
+
console.warn(`[artes] Warning: logo not found at "${resolved}". Falling back to default logo.`);
|
|
85
|
+
return defaultLogoPath();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function defaultLogoPath() {
|
|
92
|
+
return path.resolve(process.cwd(), "node_modules", "artes", "assets", "logo.png");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isRemoteUrl(logoConfig) {
|
|
96
|
+
return typeof logoConfig === "string" && (logoConfig.startsWith("http://") || logoConfig.startsWith("https://"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readFileAsBuffer(filePath) {
|
|
100
|
+
const buffer = fs.readFileSync(filePath);
|
|
101
|
+
const ext = path.extname(filePath).replace(".", "").toLowerCase();
|
|
102
|
+
const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
103
|
+
const filename = path.basename(filePath);
|
|
104
|
+
return { buffer, mime, filename };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fetchRemoteLogo(url, redirectCount = 0) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
if (redirectCount > 5) return reject(new Error("Too many redirects"));
|
|
110
|
+
|
|
111
|
+
const client = url.startsWith("https://") ? https : http;
|
|
112
|
+
|
|
113
|
+
client.get(url, (res) => {
|
|
114
|
+
|
|
115
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
116
|
+
return fetchRemoteLogo(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (res.statusCode !== 200) {
|
|
120
|
+
res.resume();
|
|
121
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const contentType = res.headers["content-type"] || "";
|
|
125
|
+
const mime = contentType.split(";")[0].trim();
|
|
126
|
+
|
|
127
|
+
if (!mime.startsWith("image/")) {
|
|
128
|
+
res.resume();
|
|
129
|
+
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
|
+
}
|
|
131
|
+
|
|
132
|
+
const urlPath = new URL(url).pathname;
|
|
133
|
+
const ext = path.extname(urlPath) || inferExtFromMime(mime);
|
|
134
|
+
const filename = path.basename(urlPath) || `logo${ext}`;
|
|
135
|
+
|
|
136
|
+
const chunks = [];
|
|
137
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
138
|
+
res.on("end", () => resolve({ buffer: Buffer.concat(chunks), mime, filename }));
|
|
139
|
+
res.on("error", reject);
|
|
140
|
+
}).on("error", reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function inferExtFromMime(mime) {
|
|
145
|
+
const map = { "image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/svg+xml": ".svg", "image/webp": ".webp" };
|
|
146
|
+
return map[mime] || ".png";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function generateCss(report, today, reportName, logoUrl) {
|
|
150
|
+
return `.side-nav__brand{background:url('${logoUrl}') no-repeat center left !important;background-size:70px 70px !important;height:80px;width:200px;display:flex !important;align-items:center;padding-left:65px}.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:26px;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}';font-size:18px;font-weight:lighter;text-transform:uppercase}.widget__flex-line:first-child .widget__subtitle{font-size:14px}.widget__flex-line:not(:first-child) .widget__title{font-size:inherit;font-weight:lighter}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function injectCssAndReturn(cssPath, dynamicCss) {
|
|
154
|
+
let css = fs.readFileSync(cssPath, "utf8");
|
|
155
|
+
|
|
156
|
+
const startIdx = css.indexOf(CSS_START_MARKER);
|
|
157
|
+
const endIdx = css.indexOf(CSS_END_MARKER);
|
|
158
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
159
|
+
css = css.slice(0, startIdx) + css.slice(endIdx + CSS_END_MARKER.length);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
css = css.trimEnd();
|
|
163
|
+
css += `\n${CSS_START_MARKER}\n${dynamicCss}\n${CSS_END_MARKER}\n`;
|
|
164
|
+
return css;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function updateSingleFileHtml(htmlPath, report, reportName, faviconDataUrl, cssDataUrl) {
|
|
168
|
+
let html = fs.readFileSync(htmlPath, "utf8");
|
|
169
|
+
|
|
170
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
|
|
171
|
+
|
|
172
|
+
html = html.replace(
|
|
173
|
+
/<link rel="icon" href="data:image\/[^"]+"/,
|
|
174
|
+
`<link rel="icon" href="${faviconDataUrl}"`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
html = html.replace(
|
|
178
|
+
/<link rel="stylesheet" type="text\/css" href="data:text\/css;base64,[^"]+"/,
|
|
179
|
+
`<link rel="stylesheet" type="text/css" href="${cssDataUrl}"`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
fs.writeFileSync(htmlPath, html, "utf8");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function updateHtml(htmlPath, report, reportName, faviconDataUrl) {
|
|
186
|
+
let html = fs.readFileSync(htmlPath, "utf8");
|
|
187
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>ARTES REPORT</title>`);
|
|
188
|
+
|
|
189
|
+
html = html.replace(/<link rel="icon" href=".*?">/, `<link rel="icon" href="${faviconDataUrl}">`);
|
|
190
|
+
fs.writeFileSync(htmlPath, html, "utf8");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { reportCustomizer };
|
|
@@ -42,6 +42,15 @@ function showHelp() {
|
|
|
42
42
|
|
|
43
43
|
🗜️ --zip Zip the report folder after generation
|
|
44
44
|
Usage: artes -r --zip
|
|
45
|
+
|
|
46
|
+
🖼️ --logo Set a custom logo in the report sidebar
|
|
47
|
+
Usage: artes --logo logo.png
|
|
48
|
+
|
|
49
|
+
🏢 --brandName Set the brand name displayed next to the logo in the report sidebar
|
|
50
|
+
Usage: artes --brandName 'My Company'
|
|
51
|
+
|
|
52
|
+
📄 --reportName Set the report name displayed on the summary widget
|
|
53
|
+
Usage: artes --reportName 'Alma UI'
|
|
45
54
|
|
|
46
55
|
📁 --features Specify one or more feature files' relative paths to run (comma-separated)
|
|
47
56
|
Usage: artes --features "tests/features/Alma, tests/features/Banan.feature"
|
|
@@ -58,6 +58,9 @@ function createProject(createYes, noDeps) {
|
|
|
58
58
|
// parallel: 0, // number - Number of parallel workers
|
|
59
59
|
// report: true // boolean - Generate report
|
|
60
60
|
// zip: false // boolean - Generate zip of report
|
|
61
|
+
// logo: "" // string - Custom logo for the report sidebar. Accepts an absolute path, a relative path, or a direct image URL
|
|
62
|
+
// brandName: "" // string - Brand name displayed next to the logo in the report sidebar
|
|
63
|
+
// reportName: "" // string - Report name displayed on the summary widget
|
|
61
64
|
// reportSuccess: false, // boolean - Add screenshots and video records to report also for success test cases
|
|
62
65
|
// trace: false, // boolean - Enable tracing
|
|
63
66
|
// reportWithTrace: false, // boolean - Include trace in report
|
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const archiver = require("archiver");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
const { moduleConfig } = require("../imports/commons");
|
|
6
|
+
const { reportCustomizer } = require("../../helper/controller/reportCustomizer");
|
|
6
7
|
|
|
7
8
|
function generateReport() {
|
|
8
9
|
try {
|
|
@@ -29,6 +30,17 @@ function generateReport() {
|
|
|
29
30
|
`📋 Report generated successfully in ${moduleConfig.reportPath}!`,
|
|
30
31
|
);
|
|
31
32
|
|
|
33
|
+
let customizerDone = false;
|
|
34
|
+
let customizerError = null;
|
|
35
|
+
|
|
36
|
+
Promise.resolve(reportCustomizer())
|
|
37
|
+
.then(() => { customizerDone = true; })
|
|
38
|
+
.catch((err) => { customizerError = err; customizerDone = true; });
|
|
39
|
+
|
|
40
|
+
require("deasync").loopWhile(() => !customizerDone);
|
|
41
|
+
|
|
42
|
+
if (customizerError) throw customizerError;
|
|
43
|
+
|
|
32
44
|
if (fs.existsSync(moduleConfig.reportPath) && process.env.ZIP === "true") {
|
|
33
45
|
console.log(`🗜️ Zipping report folder...`);
|
|
34
46
|
|
package/src/hooks/hooks.js
CHANGED
|
@@ -20,6 +20,9 @@ const path = require("path");
|
|
|
20
20
|
const { moduleConfig, saveVar } = require("artes/src/helper/imports/commons");
|
|
21
21
|
require("allure-cucumberjs");
|
|
22
22
|
const allure = require("allure-js-commons");
|
|
23
|
+
const ffprobe = require('ffprobe-static');
|
|
24
|
+
const ffmpegPath = require('ffmpeg-static');
|
|
25
|
+
const { execSync } = require('child_process');
|
|
23
26
|
|
|
24
27
|
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];
|
|
25
28
|
|
|
@@ -147,7 +150,8 @@ After(async function ({result, pickle}) {
|
|
|
147
150
|
|
|
148
151
|
await attachResponse(allure.attachment);
|
|
149
152
|
context.response = await {};
|
|
150
|
-
|
|
153
|
+
|
|
154
|
+
Object.keys(context.vars).length > 0 && allure.attachment('Variables', JSON.stringify(context.vars, null, 2), 'application/json')
|
|
151
155
|
|
|
152
156
|
const shouldReport =
|
|
153
157
|
cucumberConfig.default.successReport || result?.status !== Status.PASSED;
|
|
@@ -199,9 +203,30 @@ After(async function ({result, pickle}) {
|
|
|
199
203
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
200
204
|
|
|
201
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')
|
|
202
209
|
const webmBuffer = fs.readFileSync(videoPath);
|
|
203
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
|
+
}
|
|
204
228
|
}
|
|
229
|
+
|
|
205
230
|
}
|
|
206
231
|
}
|
|
207
232
|
});
|