@testomatio/reporter 1.1.0-beta-2 → 1.2.0-beta-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/lib/_ArtifactStorageOld.js +142 -0
- package/lib/adapter/codecept.js +2 -2
- package/lib/adapter/cucumber/current.js +3 -3
- package/lib/adapter/cucumber/legacy.js +2 -2
- package/lib/adapter/jest.js +16 -2
- package/lib/adapter/mocha.js +43 -15
- package/lib/adapter/playwright.js +21 -33
- package/lib/artifactStorage.js +25 -0
- package/lib/client.js +21 -18
- package/lib/constants.js +13 -4
- package/lib/{storages/data-storage.js → dataStorage.js} +47 -122
- package/lib/{storages/logger.js → logger.js} +33 -4
- package/lib/pipe/html.js +187 -0
- package/lib/pipe/index.js +2 -0
- package/lib/reporter.js +4 -9
- package/lib/template/real-data-example.js +47 -0
- package/lib/template/template.hbs +249 -0
- package/lib/template/testomatio.hbs +720 -0
- package/lib/utils/utils.js +0 -14
- package/package.json +2 -1
- package/lib/reporter-functions.js +0 -43
- package/lib/storages/artifact-storage.js +0 -70
- package/lib/storages/key-value-storage.js +0 -58
|
@@ -3,18 +3,16 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { join } = require('path');
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
const getTestIdFromTestTitle = parseTest;
|
|
6
|
+
const JestReporter = require('./adapter/jest');
|
|
7
|
+
const { TESTOMAT_TMP_STORAGE } = require('./constants');
|
|
8
|
+
const { fileSystem } = require('./utils/utils');
|
|
9
|
+
const getTestIdFromTestTitle = require('./utils/utils').parseTest;
|
|
10
10
|
|
|
11
11
|
class DataStorage {
|
|
12
12
|
/**
|
|
13
13
|
* Creates data storage instance for specific data type.
|
|
14
14
|
* Stores data to global variable or to file depending on what is applicable for current test runner
|
|
15
15
|
* (running environment).
|
|
16
|
-
* Recommend to use composition while using this class (instead of inheritance).
|
|
17
|
-
* ! Also the class which will use data storage should be singleton (to avoid data loss).
|
|
18
16
|
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
19
17
|
*/
|
|
20
18
|
constructor(dataType) {
|
|
@@ -22,35 +20,8 @@ class DataStorage {
|
|
|
22
20
|
this.isFileStorage = true;
|
|
23
21
|
this.#refreshStorageType();
|
|
24
22
|
|
|
25
|
-
this.dataDirPath = path.join(
|
|
26
|
-
|
|
27
|
-
// set test title for mocha (could not be moved to adapter)
|
|
28
|
-
// it does not override existing hook, just add new one
|
|
29
|
-
try {
|
|
30
|
-
// @ts-ignore
|
|
31
|
-
if (!beforeEach) return;
|
|
32
|
-
// @ts-ignore-next-line
|
|
33
|
-
beforeEach(function () {
|
|
34
|
-
if (this.currentTest?.__mocha_id__) {
|
|
35
|
-
global.testTitle = this.currentTest.fullTitle();
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
} catch (e) {
|
|
39
|
-
// ignore
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// set test title for playwright
|
|
43
|
-
try {
|
|
44
|
-
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
|
|
45
|
-
const { test } = require('@playwright/test');
|
|
46
|
-
// eslint-disable-next-line no-empty-pattern
|
|
47
|
-
test.beforeEach(async ({}, testInfo) => {
|
|
48
|
-
global.testTitle = testInfo.title;
|
|
49
|
-
global.testomatioRunningEnvironment = 'playwright';
|
|
50
|
-
});
|
|
51
|
-
} catch (e) {
|
|
52
|
-
// ignore
|
|
53
|
-
}
|
|
23
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataType);
|
|
24
|
+
fileSystem.createDir(this.dataDirPath);
|
|
54
25
|
}
|
|
55
26
|
|
|
56
27
|
/**
|
|
@@ -73,18 +44,15 @@ class DataStorage {
|
|
|
73
44
|
}
|
|
74
45
|
|
|
75
46
|
/**
|
|
76
|
-
* Puts any data to storage (file or global variable)
|
|
77
|
-
*
|
|
78
|
-
* @param {*} data anything you want to store (string, object, array, etc)
|
|
47
|
+
* Puts any data to storage (file or global variable)
|
|
48
|
+
* @param {*} data anything you want to store
|
|
79
49
|
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
80
50
|
* @returns
|
|
81
51
|
*/
|
|
82
52
|
putData(data, context = null) {
|
|
83
|
-
fileSystem.createDir(this.dataDirPath);
|
|
84
|
-
|
|
85
53
|
this.#refreshStorageType();
|
|
86
|
-
|
|
87
|
-
let testId = this.#tryToRetrieveTestId(context) ||
|
|
54
|
+
|
|
55
|
+
let testId = this.#tryToRetrieveTestId(context) || null;
|
|
88
56
|
|
|
89
57
|
if (this.runningEnvironment === 'codeceptjs') {
|
|
90
58
|
this.isFileStorage = false;
|
|
@@ -92,7 +60,7 @@ class DataStorage {
|
|
|
92
60
|
}
|
|
93
61
|
|
|
94
62
|
if (this.runningEnvironment === 'jest') {
|
|
95
|
-
testId = testId ??
|
|
63
|
+
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
96
64
|
}
|
|
97
65
|
|
|
98
66
|
// logs in playwright are gathered by pw framework itself
|
|
@@ -106,7 +74,7 @@ class DataStorage {
|
|
|
106
74
|
testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
107
75
|
|
|
108
76
|
if (!testId) {
|
|
109
|
-
debug(`No test id provided for
|
|
77
|
+
debug(`No test id provided for ${this.dataType} data: ${data}`);
|
|
110
78
|
return;
|
|
111
79
|
}
|
|
112
80
|
|
|
@@ -119,10 +87,12 @@ class DataStorage {
|
|
|
119
87
|
|
|
120
88
|
/**
|
|
121
89
|
* Returns data, stored for specific testId (or data which was stored without test id specified).
|
|
122
|
-
* This method will get data from global variable
|
|
90
|
+
* This method will get data from global variable. But if it is not available, it will try to get data from file.
|
|
91
|
+
* Thus, good approach is to remove file storage folder before each test run (and after, for sure).
|
|
123
92
|
*
|
|
124
|
-
*
|
|
125
|
-
* @
|
|
93
|
+
* Defining the execution environment is not guaranteed! Is used only as additional check.
|
|
94
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
95
|
+
* @returns
|
|
126
96
|
*/
|
|
127
97
|
getData(context) {
|
|
128
98
|
this.#refreshStorageType();
|
|
@@ -130,6 +100,9 @@ class DataStorage {
|
|
|
130
100
|
let testId = null;
|
|
131
101
|
if (typeof context === 'string') {
|
|
132
102
|
testId = this.#tryToRetrieveTestId(context) || context;
|
|
103
|
+
} else {
|
|
104
|
+
// TODO: derive testId from context
|
|
105
|
+
// testId = context...
|
|
133
106
|
}
|
|
134
107
|
|
|
135
108
|
if (!testId) {
|
|
@@ -137,20 +110,12 @@ class DataStorage {
|
|
|
137
110
|
return null;
|
|
138
111
|
}
|
|
139
112
|
|
|
140
|
-
let
|
|
141
|
-
let testDataFromGlobalVar = [];
|
|
113
|
+
let testData = '';
|
|
142
114
|
|
|
143
115
|
if (global?.testomatioDataStore) {
|
|
144
|
-
|
|
116
|
+
testData = this.#getDataFromGlobalVar(testId);
|
|
145
117
|
// these frameworks use global variable storage
|
|
146
|
-
|
|
147
|
-
if (testDataFromGlobalVar) {
|
|
148
|
-
// return as string if its log data
|
|
149
|
-
if (this.dataType === 'log' && testDataFromGlobalVar.length) return testDataFromGlobalVar.join('\n');
|
|
150
|
-
// return as array for other data types
|
|
151
|
-
if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
|
|
152
|
-
}
|
|
153
|
-
// don't return nothing if no data in global variable
|
|
118
|
+
if (testData && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) return testData;
|
|
154
119
|
}
|
|
155
120
|
|
|
156
121
|
/* condition is removed for mocha
|
|
@@ -161,16 +126,11 @@ class DataStorage {
|
|
|
161
126
|
// if (testData) return testData;
|
|
162
127
|
// }
|
|
163
128
|
|
|
164
|
-
testDataFromFile = this.#getDataFromFile(testId);
|
|
129
|
+
const testDataFromFile = this.#getDataFromFile(testId);
|
|
130
|
+
testData += testDataFromFile || '';
|
|
165
131
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return testDataFromFile;
|
|
169
|
-
}
|
|
170
|
-
debug(`No "${this.dataType}" data for test id ${testId} in both file and global variable`);
|
|
171
|
-
|
|
172
|
-
// in case no data found for test
|
|
173
|
-
return null;
|
|
132
|
+
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
133
|
+
return testData || '';
|
|
174
134
|
}
|
|
175
135
|
|
|
176
136
|
/**
|
|
@@ -206,63 +166,46 @@ class DataStorage {
|
|
|
206
166
|
this.runningEnvironment = this.getRunningEnviroment();
|
|
207
167
|
|
|
208
168
|
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
209
|
-
if (
|
|
210
|
-
// playwrigth stores logs by itself; but file storage is used for other data types for playwright
|
|
211
|
-
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') this.isFileStorage = false;
|
|
169
|
+
if (['playwright', 'codeceptjs'].includes(this.runningEnvironment)) this.isFileStorage = false;
|
|
212
170
|
}
|
|
213
171
|
|
|
214
|
-
/**
|
|
215
|
-
* @param {*} testId
|
|
216
|
-
* @returns aray of data (any type)
|
|
217
|
-
*/
|
|
218
172
|
#getDataFromGlobalVar(testId) {
|
|
219
173
|
try {
|
|
220
174
|
if (global?.testomatioDataStore[this.dataType]) {
|
|
221
175
|
const testData = global.testomatioDataStore[this.dataType][testId];
|
|
222
|
-
|
|
223
|
-
return testData ||
|
|
176
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
177
|
+
return testData || '';
|
|
224
178
|
}
|
|
225
|
-
|
|
226
|
-
return
|
|
179
|
+
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
180
|
+
return '';
|
|
227
181
|
} catch (e) {
|
|
228
182
|
// there could be no data, ignore
|
|
229
183
|
}
|
|
230
184
|
}
|
|
231
185
|
|
|
232
|
-
/**
|
|
233
|
-
*
|
|
234
|
-
* @param {*} testId
|
|
235
|
-
* @returns array of data (any type)
|
|
236
|
-
*/
|
|
237
186
|
#getDataFromFile(testId) {
|
|
238
187
|
try {
|
|
239
188
|
const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
|
|
240
189
|
if (fs.existsSync(filepath)) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return testDataArr;
|
|
190
|
+
const testData = fs.readFileSync(filepath, 'utf-8');
|
|
191
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
192
|
+
return testData || '';
|
|
245
193
|
}
|
|
246
|
-
|
|
247
|
-
return
|
|
194
|
+
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
195
|
+
return '';
|
|
248
196
|
} catch (e) {
|
|
249
197
|
// there could be no data, ignore
|
|
250
198
|
}
|
|
251
|
-
return
|
|
199
|
+
return '';
|
|
252
200
|
}
|
|
253
201
|
|
|
254
|
-
/**
|
|
255
|
-
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
256
|
-
* @param {*} data
|
|
257
|
-
* @param {*} testId
|
|
258
|
-
*/
|
|
259
202
|
#putDataToGlobalVar(data, testId) {
|
|
260
|
-
debug(
|
|
203
|
+
debug('Saving data to global variable for test', testId, ':\n', data, '\n');
|
|
261
204
|
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
262
205
|
if (!global.testomatioDataStore?.[this.dataType]) global.testomatioDataStore[this.dataType] = {};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
206
|
+
global.testomatioDataStore?.[this.dataType][testId] // eslint-disable-line no-unused-expressions
|
|
207
|
+
? (global.testomatioDataStore[this.dataType][testId] += `\n${data}`)
|
|
208
|
+
: (global.testomatioDataStore[this.dataType][testId] = data);
|
|
266
209
|
}
|
|
267
210
|
|
|
268
211
|
#putDataToFile(data, testId) {
|
|
@@ -272,36 +215,18 @@ class DataStorage {
|
|
|
272
215
|
if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
|
|
273
216
|
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
274
217
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
} else {
|
|
279
|
-
fs.writeFileSync(filepath, data, 'utf-8');
|
|
280
|
-
}
|
|
218
|
+
// TODO: handle multiple invocations of JSON.stringify.
|
|
219
|
+
// UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
|
|
220
|
+
fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
|
|
281
221
|
}
|
|
282
222
|
}
|
|
283
223
|
|
|
284
|
-
module.exports = DataStorage;
|
|
224
|
+
module.exports.DataStorage = DataStorage;
|
|
285
225
|
|
|
286
226
|
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
287
227
|
// prevent blocking and improve performance (probably queue usage will be required)
|
|
288
228
|
|
|
289
229
|
// TODO: rewrite client - everything regarding storing artifacts
|
|
290
230
|
// TODO: try to define adapter inside client
|
|
231
|
+
// TODO: use .env
|
|
291
232
|
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
292
|
-
|
|
293
|
-
/* does not work for Playwright
|
|
294
|
-
try {
|
|
295
|
-
const { test } = require('@playwright/test');
|
|
296
|
-
test.beforeEach(async ({}, testInfo) => {
|
|
297
|
-
global.testTitle = testInfo.title;
|
|
298
|
-
global.testomatioRunningEnvironment = 'playwright';
|
|
299
|
-
});
|
|
300
|
-
} catch (e) {
|
|
301
|
-
// ignore
|
|
302
|
-
}
|
|
303
|
-
// Playwright storage works fine only when running tests from a single file;
|
|
304
|
-
// otherwise it is not possible to define test id/title, it is overwritten
|
|
305
|
-
TODO: the only way implementation for Pw for now is to pass testInfo inside test
|
|
306
|
-
// (and process this testInfo inside "put" function)
|
|
307
|
-
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const debug = require('debug')('@testomatio/reporter:logger');
|
|
3
|
-
const DataStorage = require('./
|
|
3
|
+
const { DataStorage } = require('./dataStorage');
|
|
4
4
|
|
|
5
5
|
const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
|
|
6
6
|
const LEVELS = {
|
|
@@ -38,6 +38,20 @@ class Logger {
|
|
|
38
38
|
if (!Logger.instance) {
|
|
39
39
|
Logger.instance = this;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
// add beforeEach hook for mocha. it does not override existing hook, just add new one
|
|
43
|
+
try {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
if (!beforeEach) return;
|
|
46
|
+
// @ts-ignore-next-line
|
|
47
|
+
beforeEach(function () {
|
|
48
|
+
if (this.currentTest?.__mocha_id__) {
|
|
49
|
+
global.testTitle = this.currentTest.fullTitle();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -47,6 +61,9 @@ class Logger {
|
|
|
47
61
|
* @param {...any} values
|
|
48
62
|
*/
|
|
49
63
|
step(strings, ...values) {
|
|
64
|
+
// get testId for mocha
|
|
65
|
+
const context = global.testTitle ?? null;
|
|
66
|
+
|
|
50
67
|
let logs = '';
|
|
51
68
|
for (let i = 0; i < strings.length; i++) {
|
|
52
69
|
logs += strings[i];
|
|
@@ -55,7 +72,7 @@ class Logger {
|
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
logs = chalk.blue(`> ${logs}`);
|
|
58
|
-
this.#dataStorage.putData(logs);
|
|
75
|
+
this.#dataStorage.putData(logs, context);
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
/**
|
|
@@ -100,6 +117,11 @@ class Logger {
|
|
|
100
117
|
templateLiteralLog(strings, ...args) {
|
|
101
118
|
if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
|
|
102
119
|
if (Array.isArray(args)) args = args.filter(item => item !== '');
|
|
120
|
+
// entity which is used to define testId
|
|
121
|
+
let context = null;
|
|
122
|
+
|
|
123
|
+
// get testId for mocha
|
|
124
|
+
context = global.testTitle ?? null;
|
|
103
125
|
|
|
104
126
|
let logs;
|
|
105
127
|
// this block means tagged template is used (syntax like $`text ${someVar}`)
|
|
@@ -124,7 +146,7 @@ class Logger {
|
|
|
124
146
|
logs = this.#stringifyLogs(strings, ...args);
|
|
125
147
|
}
|
|
126
148
|
this.#originalUserLogger.log(logs);
|
|
127
|
-
this.#dataStorage.putData(logs);
|
|
149
|
+
this.#dataStorage.putData(logs, context);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
/**
|
|
@@ -136,12 +158,18 @@ class Logger {
|
|
|
136
158
|
#logWrapper(argsArray, level) {
|
|
137
159
|
if (!argsArray.length) return;
|
|
138
160
|
|
|
161
|
+
// entity which is used to define testId
|
|
162
|
+
let context = null;
|
|
163
|
+
|
|
164
|
+
// get context for mocha
|
|
165
|
+
context = global.testTitle ?? null;
|
|
166
|
+
|
|
139
167
|
const severity = LEVELS[level].severity;
|
|
140
168
|
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
141
169
|
|
|
142
170
|
const logs = this.#stringifyLogs(...argsArray);
|
|
143
171
|
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
144
|
-
this.#dataStorage.putData(colorizedLogs);
|
|
172
|
+
this.#dataStorage.putData(colorizedLogs, context);
|
|
145
173
|
try {
|
|
146
174
|
// level.toLowerCase() represents method name (log, warn, error, etc)
|
|
147
175
|
this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
|
|
@@ -241,6 +269,7 @@ class Logger {
|
|
|
241
269
|
|
|
242
270
|
Logger.instance = null;
|
|
243
271
|
|
|
272
|
+
// module.exports.Logger = Logger;
|
|
244
273
|
const logger = new Logger();
|
|
245
274
|
|
|
246
275
|
module.exports = logger;
|
package/lib/pipe/html.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:html');
|
|
2
|
+
const merge = require('lodash.merge');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const handlebars = require('handlebars');
|
|
7
|
+
|
|
8
|
+
const { fileSystem, isSameTest } = require('../utils/utils');
|
|
9
|
+
const { HTML_REPORT } = require('../constants');
|
|
10
|
+
|
|
11
|
+
class HtmlPipe {
|
|
12
|
+
constructor(params, store = {}) {
|
|
13
|
+
this.store = store || {};
|
|
14
|
+
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
15
|
+
this.apiKey = params.apiKey || process.env.TESTOMATIO;
|
|
16
|
+
this.isHtml = process.env.TESTOMATIO_HTML_REPORT_SAVE;
|
|
17
|
+
this.data = {};
|
|
18
|
+
|
|
19
|
+
debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
20
|
+
if (!this.apiKey) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.isEnabled = false;
|
|
25
|
+
this.htmlOutputPath = "";
|
|
26
|
+
this.tests = [];
|
|
27
|
+
this.data = {};
|
|
28
|
+
|
|
29
|
+
if (this.isHtml) {
|
|
30
|
+
this.isEnabled = true;
|
|
31
|
+
this.htmlReportDir = process.env.TESTOMATIO_HTML_REPORT_FOLDER || HTML_REPORT.FOLDER;
|
|
32
|
+
|
|
33
|
+
if (process.env.TESTOMATIO_HTML_FILENAME && process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
|
|
34
|
+
this.htmlReportName = process.env.TESTOMATIO_HTML_FILENAME
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.env.TESTOMATIO_HTML_FILENAME && !process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
|
|
38
|
+
console.log(
|
|
39
|
+
chalk.blue(`The name must include the extension ".html". The default report name is used!`)
|
|
40
|
+
);
|
|
41
|
+
this.htmlReportName = HTML_REPORT.REPORT_DEFAULT_NAME;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!process.env.TESTOMATIO_HTML_FILENAME) {
|
|
45
|
+
this.htmlReportName = HTML_REPORT.REPORT_DEFAULT_NAME;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.templateFolderPath = path.resolve(__dirname, '..', 'template');
|
|
49
|
+
this.templateHtmlPath = path.resolve(this.templateFolderPath, HTML_REPORT.TEMPLATE_NAME);
|
|
50
|
+
this.htmlOutputPath = path.join(this.htmlReportDir, this.htmlReportName);
|
|
51
|
+
// create a new folder for the HTML reports
|
|
52
|
+
fileSystem.createDir(this.htmlReportDir);
|
|
53
|
+
|
|
54
|
+
debug(
|
|
55
|
+
chalk.yellow('HTML Pipe:'),
|
|
56
|
+
`Save HTML report: ${this.isEnabled}`,
|
|
57
|
+
`HTML report folder: ${this.htmlReportDir}, report name: ${this.htmlReportName}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async createRun() {
|
|
63
|
+
// empty
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateRun() {
|
|
67
|
+
// empty
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add test data to the result array for saving. As a result of this function, we get a result object to save.
|
|
72
|
+
* @param {Object} test - object which includes each test entry.
|
|
73
|
+
*/
|
|
74
|
+
addTest(test) {
|
|
75
|
+
if (!this.isEnabled) return;
|
|
76
|
+
|
|
77
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
78
|
+
// update if they were already added
|
|
79
|
+
if (index >= 0) {
|
|
80
|
+
this.tests[index] = merge(this.tests[index], test);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.tests.push(test);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async finishRun(runParams) {
|
|
88
|
+
if (!this.isEnabled) return;
|
|
89
|
+
|
|
90
|
+
if (this.isHtml) {
|
|
91
|
+
this.data = {
|
|
92
|
+
runId: this.store.runId,
|
|
93
|
+
status: runParams.status,
|
|
94
|
+
parallel: runParams.isParallel,
|
|
95
|
+
runUrl: this.store.runUrl,
|
|
96
|
+
executionTime: testExecutionSumTime(this.tests),
|
|
97
|
+
executionDate: getCurrentDateTimeFormatted(),
|
|
98
|
+
tests: this.tests
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// GENERATE HTML reports based on the results data
|
|
102
|
+
this.buildReport(this.data);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
buildReport(data) {
|
|
107
|
+
debug('HTML tests data:', data);
|
|
108
|
+
|
|
109
|
+
if (!this.htmlOutputPath && this.htmlOutputPath !== "") {
|
|
110
|
+
console.log(chalk.yellow(`HTML export path is not set, ignoring...`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(chalk.yellow(`The test results will be added to the HTML report. It will take some time...`));
|
|
115
|
+
// generate output HTML based on the template
|
|
116
|
+
const html = this.#generateHTMLReport(data);
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(this.htmlOutputPath, html, 'utf-8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#generateHTMLReport(data) {
|
|
122
|
+
if (!this.templateHtmlPath) {
|
|
123
|
+
console.log(chalk.red(`HTML template not found. Report generation is impossible!`))
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const templateSource = fs.readFileSync(this.templateHtmlPath, 'utf8');
|
|
128
|
+
this.#loadReportHelpers();
|
|
129
|
+
try {
|
|
130
|
+
const template = handlebars.compile(templateSource);
|
|
131
|
+
|
|
132
|
+
console.log(chalk.green(`Generated HTML report saved in file: ${this.htmlOutputPath}`));
|
|
133
|
+
return template(data);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.log('Unknown HTML report generation error: ', e);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#loadReportHelpers() {
|
|
141
|
+
handlebars.registerHelper('getTestsByStatus', (tests, status) =>
|
|
142
|
+
tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
handlebars.registerHelper('json', (obj) => JSON.stringify(obj));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
toString() {
|
|
149
|
+
return 'HTML Reporter';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function testExecutionSumTime(tests) {
|
|
154
|
+
const totalMilliseconds = tests.reduce((sum, test) => {
|
|
155
|
+
if (typeof test.run_time === 'number') {
|
|
156
|
+
return sum + test.run_time;
|
|
157
|
+
}
|
|
158
|
+
return sum;
|
|
159
|
+
}, 0);
|
|
160
|
+
|
|
161
|
+
return formatDuration(totalMilliseconds);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatDuration(duration) {
|
|
165
|
+
const milliseconds = duration % 1000;
|
|
166
|
+
duration = (duration - milliseconds) / 1000;
|
|
167
|
+
const seconds = duration % 60;
|
|
168
|
+
duration = (duration - seconds) / 60;
|
|
169
|
+
const minutes = duration % 60;
|
|
170
|
+
const hours = (duration - minutes) / 60;
|
|
171
|
+
|
|
172
|
+
return `${hours}h ${minutes}m ${seconds}s ${milliseconds}ms`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getCurrentDateTimeFormatted() {
|
|
176
|
+
const currentDate = new Date();
|
|
177
|
+
const day = currentDate.getDate().toString().padStart(2, '0');
|
|
178
|
+
const month = (currentDate.getMonth() + 1).toString().padStart(2, '0');
|
|
179
|
+
const year = currentDate.getFullYear();
|
|
180
|
+
const hours = currentDate.getHours().toString().padStart(2, '0');
|
|
181
|
+
const minutes = currentDate.getMinutes().toString().padStart(2, '0');
|
|
182
|
+
const seconds = currentDate.getSeconds().toString().padStart(2, '0');
|
|
183
|
+
|
|
184
|
+
return `(${day}/${month}/${year} ${hours}:${minutes}:${seconds})`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = HtmlPipe;
|
package/lib/pipe/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const TestomatioPipe = require('./testomatio');
|
|
|
6
6
|
const GitHubPipe = require('./github');
|
|
7
7
|
const GitLabPipe = require('./gitlab');
|
|
8
8
|
const CsvPipe = require('./csv');
|
|
9
|
+
const HtmlPipe = require('./html');
|
|
9
10
|
|
|
10
11
|
function PipeFactory(params, opts) {
|
|
11
12
|
const extraPipes = [];
|
|
@@ -43,6 +44,7 @@ function PipeFactory(params, opts) {
|
|
|
43
44
|
new GitHubPipe(params, opts),
|
|
44
45
|
new GitLabPipe(params, opts),
|
|
45
46
|
new CsvPipe(params, opts),
|
|
47
|
+
new HtmlPipe(params, opts),
|
|
46
48
|
...extraPipes,
|
|
47
49
|
];
|
|
48
50
|
|
package/lib/reporter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const logger = require('./
|
|
1
|
+
const logger = require('./logger');
|
|
2
2
|
const TestomatClient = require('./client');
|
|
3
3
|
const TRConstants = require('./constants');
|
|
4
|
+
const TRArtifacts = require('./_ArtifactStorageOld');
|
|
4
5
|
|
|
5
6
|
const log = logger.templateLiteralLog.bind(logger);
|
|
6
7
|
const step = logger.step.bind(logger);
|
|
7
|
-
const reporterFunctions = require('./reporter-functions');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
logger,
|
|
@@ -13,11 +13,6 @@ module.exports = {
|
|
|
13
13
|
step,
|
|
14
14
|
TestomatClient,
|
|
15
15
|
TRConstants,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
module.exports.testomat = {
|
|
19
|
-
artifact: reporterFunctions.artifact,
|
|
20
|
-
log: reporterFunctions.log,
|
|
21
|
-
step: reporterFunctions.step,
|
|
22
|
-
meta: reporterFunctions.keyValue,
|
|
16
|
+
TRArtifacts,
|
|
17
|
+
addArtifact: TRArtifacts.artifact,
|
|
23
18
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// TODO: use as example for the unit-tests
|
|
2
|
+
|
|
3
|
+
const result = {
|
|
4
|
+
"runId": "51eb2798",
|
|
5
|
+
"status": "failed",
|
|
6
|
+
"runUrl": "https://beta.testomat.io/projects/codecept-new-mode-exmple/runs/51eb2798/report",
|
|
7
|
+
"executionTime": "0 seconds",
|
|
8
|
+
"executionDate": "(30/07/2023 11:11:11)",
|
|
9
|
+
"tests": [{
|
|
10
|
+
"files": [],
|
|
11
|
+
"steps": "\u001b[32m\u001b[1mOn TodosPage: goto \u001b[22m\u001b[39m\n",
|
|
12
|
+
"status": "passed",
|
|
13
|
+
"stack": "I execute script () => sessionStorage.clear()",
|
|
14
|
+
"example": null,
|
|
15
|
+
"code": null,
|
|
16
|
+
"title": "Create a new todo item @T50e82737",
|
|
17
|
+
"suite_title": "Create Tasks @step:06 @smoke @story:12 @S2f5c1942",
|
|
18
|
+
"test_id": "50e82737",
|
|
19
|
+
"message": "",
|
|
20
|
+
"run_time": 121,
|
|
21
|
+
"artifacts": [],
|
|
22
|
+
"api_key": "tstmt_gRqrhBUaVxTpezGpZjRmlahOeqcRBBbDMA1692050199",
|
|
23
|
+
"create": false
|
|
24
|
+
}, {
|
|
25
|
+
"files": [{
|
|
26
|
+
"path": "/home/codeceptjs-testomat-example/codeceptJS/output/Create_2_@T5b8d1186.failed.png",
|
|
27
|
+
"type": "image/png"
|
|
28
|
+
}],
|
|
29
|
+
"steps": "I am on page \"http://todomvc.com/examples/angularjs/#/\"",
|
|
30
|
+
"status": "failed",
|
|
31
|
+
"stack": "I am on page \"http://todomvc.com/examples/angularjs/#/\"",
|
|
32
|
+
"example": null,
|
|
33
|
+
"code": null,
|
|
34
|
+
"title": "Create a new todo item part 2 @T5b8d1186",
|
|
35
|
+
"suite_title": "@second Create Tasks @step:07 @smoke @story:13 @S2f5c1942",
|
|
36
|
+
"test_id": "5b8d1186",
|
|
37
|
+
"message": "expected expected number of visible is 2, but found 1 \"1\" to equal \"2\"",
|
|
38
|
+
"run_time": 176,
|
|
39
|
+
"artifacts": [null],
|
|
40
|
+
"api_key": "tstmt_gRqrhBUaVxTpezGpZjRmlahOeqcRBBbDMA1692050199",
|
|
41
|
+
"create": false
|
|
42
|
+
}]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
result
|
|
47
|
+
}
|