@testomatio/reporter 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +23 -0
- package/README.md +68 -3
- package/lib/adapter/codecept.js +1 -1
- package/lib/adapter/playwright.js +1 -1
- package/lib/bin/startTest.js +8 -2
- package/lib/client.js +92 -50
- package/lib/fileUploader.js +7 -2
- package/package.json +1 -1
package/Changelog.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
# 0.4.4
|
|
2
|
+
|
|
3
|
+
* Fixed returning 0 exit code when a process fails when running tests in parallel via `start-test-run`. Previously was using the last exit code returned by a process. Currently prefers the highest exit code that was returned by a process.
|
|
4
|
+
|
|
5
|
+
# 0.4.3
|
|
6
|
+
|
|
7
|
+
* Added `TESTOMATIO_DISABLE_ARTIFACTS` env variable to disable publishing artifacts.
|
|
8
|
+
|
|
9
|
+
# 0.4.2
|
|
10
|
+
|
|
11
|
+
* print version of reporter
|
|
12
|
+
* print number of uploaded artifacts
|
|
13
|
+
* print access mode for uploaded artifacts
|
|
14
|
+
|
|
15
|
+
# 0.4.1
|
|
16
|
+
|
|
17
|
+
Added `global.testomatioArtifacts = []` array which can be used to add arbitrary artifacts to a report.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// inside a running test:
|
|
21
|
+
global.testomatioArtifacts.push('file/to/upload.png');
|
|
22
|
+
```
|
|
23
|
+
|
|
1
24
|
# 0.4.0
|
|
2
25
|
|
|
3
26
|
* Playwright: Introduced playwright/test support with screenshots and video artifacts
|
package/README.md
CHANGED
|
@@ -169,9 +169,9 @@ Add environments to run by providing `TESTOMATIO_ENV` as comma seperated values:
|
|
|
169
169
|
TESTOMATIO={apiKey} TESTOMATIO_ENV="Windows, Chrome" <actual run command>
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
## Attaching
|
|
172
|
+
## Attaching Test Artifacts
|
|
173
173
|
|
|
174
|
-
To save a
|
|
174
|
+
To save a test artifacts (screenshots and videos) of a failed test use S3 storage.
|
|
175
175
|
Please note, that the **storage is not connected to Testomatio**.
|
|
176
176
|
This allows you to store your artifacts on your own account and not expose S3 credentials.
|
|
177
177
|
|
|
@@ -183,12 +183,77 @@ To save screenshots provide a configuration for S3 bucket via environment variab
|
|
|
183
183
|
- **S3_SECRET_ACCESS_KEY** - Secret.
|
|
184
184
|
- **S3_ENDPOINT** - for providers other than AWS
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
By default tests artifacts are uploaded to bucket with `public-read` permission.
|
|
187
|
+
In this case uploaded files will be publicly accessible in Internet.
|
|
188
|
+
These public links will be used by Testomat.io to display images and videos.
|
|
189
|
+
|
|
190
|
+
To upload files with `private` access bucket add `TESTOMATIO_PRIVATE_ARTIFACTS=1` environment value.
|
|
191
|
+
Then update provide the same S3 credentials in "Settings > Artifacts" section of a Testomat.io project,
|
|
192
|
+
so Testomat.io could connect to the same bucket and fetch uploaded artifacts.
|
|
193
|
+
Links to files will be pre-signed and expires automatically in 10 minutes.
|
|
194
|
+
|
|
195
|
+
Example upload configuration in environment variables
|
|
196
|
+
|
|
197
|
+
##### AWS
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
TESTOMATIO_PRIVATE_ARTIFACTS=1
|
|
201
|
+
S3_ACCESS_KEY_ID=11111111111111111111
|
|
202
|
+
S3_SECRET_ACCESS_KEY=2222222222222222222222222222222222222222222
|
|
203
|
+
S3_BUCKET=artifacts
|
|
204
|
+
S3_REGION=us-west-1
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
##### DigitalOcean
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
TESTOMATIO_PRIVATE_ARTIFACTS=1
|
|
211
|
+
S3_ENDPOINT=https://ams3.digitaloceanspaces.com
|
|
212
|
+
S3_ACCESS_KEY_ID=11111111111111111111
|
|
213
|
+
S3_SECRET_ACCESS_KEY=2222222222222222222222222222222222222222222
|
|
214
|
+
S3_BUCKET=artifacts
|
|
215
|
+
S3_REGION=ams3
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
##### Minio
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
S3_ENDPOINT=http://company.storage.com
|
|
222
|
+
S3_ACCESS_KEY_ID=minio
|
|
223
|
+
S3_SECRET_ACCESS_KEY=minio123
|
|
224
|
+
S3_BUCKET=testomatio
|
|
225
|
+
S3_FORCE_PATH_STYLE=true
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> It is important to add S3_FORCE_PATH_STYLE var for minio setup
|
|
229
|
+
|
|
187
230
|
|
|
188
231
|
For local testing, it is recommended to store this configuration in `.env` file and load it with [dotenv](https://www.npmjs.com/package/dotenv) library.
|
|
189
232
|
|
|
190
233
|
On CI set environment variables in CI config.
|
|
191
234
|
|
|
235
|
+
Test artifacts are automatically uploaded for these test runners:
|
|
236
|
+
|
|
237
|
+
* CodeceptJS
|
|
238
|
+
* Playwright
|
|
239
|
+
|
|
240
|
+
To manually attach an artifact and upload it for a test use `global.testomatioArtifacts` array:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
// attach a picture inside a test
|
|
244
|
+
global.testomatioArtifacts.push('img/file.png');
|
|
245
|
+
// attach a picture and add a name to it
|
|
246
|
+
global.testomatioArtifacts.push({ name: 'Screenshot', path: 'img/file.png' });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Artifacts will be uploaded for the current test when it is finished.
|
|
250
|
+
|
|
251
|
+
To disable uploading artifacts add `TESTOMATIO_DISABLE_ARTIFACTS` environment variable:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
TESTOMATIO_DISABLE_ARTIFACTS=1
|
|
255
|
+
```
|
|
256
|
+
|
|
192
257
|
|
|
193
258
|
## Starting an Empty Run
|
|
194
259
|
|
package/lib/adapter/codecept.js
CHANGED
|
@@ -88,7 +88,7 @@ module.exports = (config) => {
|
|
|
88
88
|
|
|
89
89
|
event.dispatcher.on(event.all.result, async () => {
|
|
90
90
|
if (videos.length) {
|
|
91
|
-
console.log(TRConstants.APP_PREFIX, `🎞️
|
|
91
|
+
console.log(TRConstants.APP_PREFIX, `🎞️ Uploading ${videos.length} videos...`);
|
|
92
92
|
|
|
93
93
|
let promises = [];
|
|
94
94
|
for (const video of videos) {
|
|
@@ -76,7 +76,7 @@ class TestomatioReporter {
|
|
|
76
76
|
if (!this.client) return;
|
|
77
77
|
|
|
78
78
|
if (this.videos.length) {
|
|
79
|
-
console.log(Status.APP_PREFIX, `🎞️
|
|
79
|
+
console.log(Status.APP_PREFIX, `🎞️ Uploading ${this.videos.length} videos...`);
|
|
80
80
|
|
|
81
81
|
let promises = [];
|
|
82
82
|
for (const video of this.videos) {
|
package/lib/bin/startTest.js
CHANGED
|
@@ -43,6 +43,8 @@ program
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
let exitCode = 0;
|
|
47
|
+
|
|
46
48
|
const testCmds = command.split(" ");
|
|
47
49
|
console.log(APP_PREFIX, `🚀 Running`, chalk.green(command));
|
|
48
50
|
|
|
@@ -55,7 +57,9 @@ program
|
|
|
55
57
|
"⚠️ ",
|
|
56
58
|
`Runner exited with ${chalk.bold(code)}, report is ignored`
|
|
57
59
|
);
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
if (code > exitCode) exitCode = code;
|
|
62
|
+
process.exitCode = exitCode;
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
return;
|
|
@@ -71,7 +75,9 @@ program
|
|
|
71
75
|
console.log(APP_PREFIX, emoji, `Runner exited with ${chalk.bold(code)}`);
|
|
72
76
|
const status = code === 0 ? "passed" : "failed";
|
|
73
77
|
client.updateRunStatus(status, true);
|
|
74
|
-
|
|
78
|
+
|
|
79
|
+
if (code > exitCode) exitCode = code;
|
|
80
|
+
process.exitCode = exitCode;
|
|
75
81
|
});
|
|
76
82
|
});
|
|
77
83
|
});
|
package/lib/client.js
CHANGED
|
@@ -2,14 +2,15 @@ const axios = require("axios");
|
|
|
2
2
|
const JsonCycle = require("json-cycle");
|
|
3
3
|
const { URL } = require("url");
|
|
4
4
|
const createCallsiteRecord = require("callsite-record");
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
const { sep, join } = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
7
|
const chalk = require("chalk");
|
|
8
|
-
const { uploadFile } = require("./fileUploader");
|
|
8
|
+
const { isPrivate, uploadFile } = require("./fileUploader");
|
|
9
9
|
const { PASSED, FAILED, FINISHED, APP_PREFIX } = require("./constants");
|
|
10
10
|
|
|
11
11
|
const TESTOMAT_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
|
|
12
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } =
|
|
12
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } =
|
|
13
|
+
process.env;
|
|
13
14
|
|
|
14
15
|
if (TESTOMATIO_RUN) {
|
|
15
16
|
process.env.runId = TESTOMATIO_RUN;
|
|
@@ -28,6 +29,8 @@ class TestomatClient {
|
|
|
28
29
|
this.runId = process.env.runId;
|
|
29
30
|
this.queue = Promise.resolve();
|
|
30
31
|
this.axios = axios.create();
|
|
32
|
+
this.totalUploaded = 0;
|
|
33
|
+
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -37,7 +40,7 @@ class TestomatClient {
|
|
|
37
40
|
*/
|
|
38
41
|
createRun() {
|
|
39
42
|
const { runId } = process.env;
|
|
40
|
-
if (!this.apiKey) throw new Error(
|
|
43
|
+
if (!this.apiKey) throw new Error("No API key is set, can't create run");
|
|
41
44
|
const runParams = {
|
|
42
45
|
api_key: this.apiKey.trim(),
|
|
43
46
|
title: this.title,
|
|
@@ -45,10 +48,15 @@ class TestomatClient {
|
|
|
45
48
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
46
49
|
env: TESTOMATIO_ENV,
|
|
47
50
|
};
|
|
51
|
+
|
|
52
|
+
global.testomatioArtifacts = [];
|
|
53
|
+
|
|
48
54
|
if (!isValidUrl(TESTOMAT_URL.trim())) {
|
|
49
55
|
console.log(
|
|
50
56
|
APP_PREFIX,
|
|
51
|
-
chalk.red(
|
|
57
|
+
chalk.red(
|
|
58
|
+
`Error creating report on Testomat.io, report url '${TESTOMAT_URL}' is invalid`
|
|
59
|
+
)
|
|
52
60
|
);
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
@@ -56,7 +64,8 @@ class TestomatClient {
|
|
|
56
64
|
if (runId) {
|
|
57
65
|
this.runId = runId;
|
|
58
66
|
this.queue = this.queue.then(() =>
|
|
59
|
-
axios.put(`${TESTOMAT_URL.trim()}/api/reporter/${runId}`, runParams)
|
|
67
|
+
axios.put(`${TESTOMAT_URL.trim()}/api/reporter/${runId}`, runParams)
|
|
68
|
+
);
|
|
60
69
|
return Promise.resolve(runId);
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -73,10 +82,12 @@ class TestomatClient {
|
|
|
73
82
|
console.log(
|
|
74
83
|
APP_PREFIX,
|
|
75
84
|
"📊 Report created. Report ID:",
|
|
76
|
-
this.runId
|
|
85
|
+
this.runId,
|
|
86
|
+
chalk.gray(`v${this.version}`)
|
|
77
87
|
);
|
|
78
88
|
process.env.runId = this.runId;
|
|
79
|
-
})
|
|
89
|
+
})
|
|
90
|
+
)
|
|
80
91
|
.catch(() => {
|
|
81
92
|
console.log(
|
|
82
93
|
APP_PREFIX,
|
|
@@ -113,50 +124,18 @@ class TestomatClient {
|
|
|
113
124
|
let stack = "";
|
|
114
125
|
|
|
115
126
|
if (error) {
|
|
116
|
-
|
|
117
|
-
if (error.inspect) message = error.inspect();
|
|
118
|
-
|
|
119
|
-
stack = `\n${chalk.bold(message)}\n`;
|
|
120
|
-
|
|
121
|
-
// diffs for mocha, cypress, codeceptjs style
|
|
122
|
-
if (error.actual && error.expected) {
|
|
123
|
-
stack += `\n\n${chalk.bold.green("+ expected")} ${chalk.bold.red(
|
|
124
|
-
"- actual"
|
|
125
|
-
)}`;
|
|
126
|
-
stack += `\n${chalk.red(
|
|
127
|
-
`- ${error.actual.toString().split("\n").join("\n- ")}`
|
|
128
|
-
)}`;
|
|
129
|
-
stack += `\n${chalk.green(
|
|
130
|
-
`+ ${error.expected.toString().split("\n").join("\n+ ")}`
|
|
131
|
-
)}`;
|
|
132
|
-
stack += "\n\n";
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const record = createCallsiteRecord({
|
|
137
|
-
forError: error,
|
|
138
|
-
});
|
|
139
|
-
if (record) {
|
|
140
|
-
stack += record.renderSync({
|
|
141
|
-
stackFilter: (frame) =>
|
|
142
|
-
frame.getFileName().indexOf(sep) > -1 &&
|
|
143
|
-
frame.getFileName().indexOf("node_modules") < 0 &&
|
|
144
|
-
frame.getFileName().indexOf("internal") < 0,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
} catch (e) {
|
|
148
|
-
console.log(e);
|
|
149
|
-
}
|
|
127
|
+
stack = this.formatError(error);
|
|
150
128
|
}
|
|
151
129
|
if (steps) {
|
|
152
|
-
stack = stack
|
|
153
|
-
? `${steps}\n\n${chalk.bold.red(
|
|
154
|
-
"################[ Failure ]################"
|
|
155
|
-
)}\n${stack}`
|
|
156
|
-
: steps;
|
|
130
|
+
stack = this.formatSteps(stack, steps);
|
|
157
131
|
}
|
|
158
132
|
|
|
159
133
|
if (this.runId) {
|
|
134
|
+
if (Array.isArray(global.testomatioArtifacts)) {
|
|
135
|
+
files.push(...global.testomatioArtifacts);
|
|
136
|
+
global.testomatioArtifacts = [];
|
|
137
|
+
}
|
|
138
|
+
|
|
160
139
|
for (const file of files) {
|
|
161
140
|
uploadedFiles.push(uploadFile(file, this.runId));
|
|
162
141
|
}
|
|
@@ -165,6 +144,11 @@ class TestomatClient {
|
|
|
165
144
|
this.queue = this.queue
|
|
166
145
|
.then(async () => {
|
|
167
146
|
if (!this.runId) return;
|
|
147
|
+
|
|
148
|
+
global.testomatioArtifacts = [];
|
|
149
|
+
|
|
150
|
+
this.totalUploaded += uploadedFiles.filter((n) => n).length;
|
|
151
|
+
|
|
168
152
|
const json = JsonCycle.stringify({
|
|
169
153
|
api_key: this.apiKey,
|
|
170
154
|
files,
|
|
@@ -196,7 +180,7 @@ class TestomatClient {
|
|
|
196
180
|
.catch((err) => {
|
|
197
181
|
if (err.response) {
|
|
198
182
|
if (err.response.status >= 400) {
|
|
199
|
-
const data = err.response.data || { message:
|
|
183
|
+
const data = err.response.data || { message: "" };
|
|
200
184
|
console.log(
|
|
201
185
|
APP_PREFIX,
|
|
202
186
|
chalk.blue(title),
|
|
@@ -210,7 +194,12 @@ class TestomatClient {
|
|
|
210
194
|
`Report couldn't be processed: ${err.response.data.message}`
|
|
211
195
|
);
|
|
212
196
|
} else {
|
|
213
|
-
console.log(
|
|
197
|
+
console.log(
|
|
198
|
+
APP_PREFIX,
|
|
199
|
+
chalk.blue(title),
|
|
200
|
+
"Report couldn't be processed",
|
|
201
|
+
err
|
|
202
|
+
);
|
|
214
203
|
}
|
|
215
204
|
});
|
|
216
205
|
|
|
@@ -243,6 +232,15 @@ class TestomatClient {
|
|
|
243
232
|
chalk.magenta(this.runUrl)
|
|
244
233
|
);
|
|
245
234
|
}
|
|
235
|
+
|
|
236
|
+
if (this.totalUploaded > 0) {
|
|
237
|
+
console.log(
|
|
238
|
+
APP_PREFIX,
|
|
239
|
+
`🗄️ Total ${this.totalUploaded} artifacts ${
|
|
240
|
+
isPrivate ? "privately" : chalk.bold("publicly")
|
|
241
|
+
} uploaded to S3 bucket `
|
|
242
|
+
);
|
|
243
|
+
}
|
|
246
244
|
}
|
|
247
245
|
})
|
|
248
246
|
.catch((err) => {
|
|
@@ -250,6 +248,50 @@ class TestomatClient {
|
|
|
250
248
|
});
|
|
251
249
|
return this.queue;
|
|
252
250
|
}
|
|
251
|
+
|
|
252
|
+
formatSteps(stack, steps) {
|
|
253
|
+
return stack
|
|
254
|
+
? `${steps}\n\n${chalk.bold.red("################[ Failure ]################")}\n${stack}`
|
|
255
|
+
: steps;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
formatError(error, message) {
|
|
259
|
+
if (!message) message = error.message;
|
|
260
|
+
if (error.inspect) message = error.inspect();
|
|
261
|
+
|
|
262
|
+
let stack = `\n${chalk.bold(message)}\n`;
|
|
263
|
+
|
|
264
|
+
// diffs for mocha, cypress, codeceptjs style
|
|
265
|
+
if (error.actual && error.expected) {
|
|
266
|
+
stack += `\n\n${chalk.bold.green("+ expected")} ${chalk.bold.red(
|
|
267
|
+
"- actual"
|
|
268
|
+
)}`;
|
|
269
|
+
stack += `\n${chalk.red(
|
|
270
|
+
`- ${error.actual.toString().split("\n").join("\n- ")}`
|
|
271
|
+
)}`;
|
|
272
|
+
stack += `\n${chalk.green(
|
|
273
|
+
`+ ${error.expected.toString().split("\n").join("\n+ ")}`
|
|
274
|
+
)}`;
|
|
275
|
+
stack += "\n\n";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const record = createCallsiteRecord({
|
|
280
|
+
forError: error,
|
|
281
|
+
});
|
|
282
|
+
if (record) {
|
|
283
|
+
stack += record.renderSync({
|
|
284
|
+
stackFilter: (frame) =>
|
|
285
|
+
frame.getFileName().indexOf(sep) > -1 &&
|
|
286
|
+
frame.getFileName().indexOf("node_modules") < 0 &&
|
|
287
|
+
frame.getFileName().indexOf("internal") < 0,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return stack;
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.log(e);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
253
295
|
}
|
|
254
296
|
|
|
255
297
|
module.exports = TestomatClient;
|
package/lib/fileUploader.js
CHANGED
|
@@ -3,11 +3,16 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const chalk = require("chalk");
|
|
5
5
|
|
|
6
|
+
const isPrivate = process.env.TESTOMATIO_PRIVATE_ARTIFACTS;
|
|
7
|
+
|
|
6
8
|
const uploadUsingS3 = async (filePath, runId) => {
|
|
7
9
|
const region = process.env.S3_REGION;
|
|
8
10
|
const bucket = process.env.S3_BUCKET;
|
|
9
11
|
const accessKeyId = process.env.S3_ACCESS_KEY_ID;
|
|
10
12
|
const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY;
|
|
13
|
+
const isDisabled = process.env.TESTOMATIO_DISABLE_ARTIFACTS;
|
|
14
|
+
|
|
15
|
+
if (isDisabled) return;
|
|
11
16
|
|
|
12
17
|
let contentType;
|
|
13
18
|
let fileName;
|
|
@@ -34,13 +39,12 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
34
39
|
Key: fileName,
|
|
35
40
|
Body: file,
|
|
36
41
|
ContentType: contentType,
|
|
37
|
-
ACL: "public-read",
|
|
42
|
+
ACL: isPrivate ? "private" : "public-read",
|
|
38
43
|
})
|
|
39
44
|
.promise();
|
|
40
45
|
return out.Location;
|
|
41
46
|
};
|
|
42
47
|
|
|
43
|
-
|
|
44
48
|
const uploadFile = async (filePath, runId) => {
|
|
45
49
|
try {
|
|
46
50
|
if (process.env.S3_BUCKET) {
|
|
@@ -54,4 +58,5 @@ const uploadFile = async (filePath, runId) => {
|
|
|
54
58
|
|
|
55
59
|
module.exports = {
|
|
56
60
|
uploadFile,
|
|
61
|
+
isPrivate,
|
|
57
62
|
};
|