@testomatio/reporter 1.1.0-beta-3 → 1.1.0-beta.label-assign

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 CHANGED
@@ -1,4 +1,16 @@
1
1
  <!-- pending release updates -->
2
+ # 1.1.0
3
+
4
+ * Assign Run by label:
5
+
6
+ ```
7
+ TESTOMATIO={API_KEY} TESTOMATIO_LABEL="release,module:checkout" <actual run command>
8
+ ```
9
+
10
+ # 1.0.18
11
+
12
+ * Fixed stack traces for CodeceptJS
13
+
2
14
  # 1.0.17
3
15
 
4
16
  Renamed `TESTOMATIO_STACK_FILTER` to `TESTOMATIO_STACK_IGNORE`
@@ -147,9 +147,6 @@ function CodeceptReporter(config) {
147
147
  failedTests.push(id || title);
148
148
  let testId = parseTest(tags);
149
149
  const testObj = getTestAndMessage(title);
150
- if (error && error.stack && test.steps && test.steps.length) {
151
- error.stack = test.steps[test.steps.length - 1].line();
152
- }
153
150
 
154
151
  const files = [];
155
152
  if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
@@ -10,6 +10,7 @@ const testomatioReporter = on => {
10
10
  const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
11
11
 
12
12
  on('before:run', async (run) => {
13
+ // TODO: looks like client.env does not exist
13
14
  if (!client.env) {
14
15
  client.env = `${run.browser.displayName},${run.system.osName}`
15
16
  }
@@ -48,10 +49,16 @@ const testomatioReporter = on => {
48
49
  }
49
50
  }
50
51
 
51
- const screenshots = results.screenshots
52
- .filter(screenshot => screenshot.path.includes(title))
53
- .filter(screenshot => screenshot.testAttemptIndex === lastAttemptIndex)
54
- .map(screenshot => screenshot.path);
52
+ const screenshots = Array.isArray(results.screenshots)
53
+ ? results.screenshots
54
+ .filter(
55
+ screenshot =>
56
+ screenshot?.path &&
57
+ screenshot?.path.includes(title) &&
58
+ screenshot?.takenAt
59
+ )
60
+ .map(screenshot => screenshot.path)
61
+ : [];
55
62
 
56
63
  const files = [...videos, ...screenshots];
57
64
 
@@ -53,6 +53,12 @@ program
53
53
 
54
54
  let exitCode = 0;
55
55
 
56
+ if (!command.split) {
57
+ process.exitCode = 255;
58
+ console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
59
+ return;
60
+ }
61
+
56
62
  const client = new TestomatClient({ apiKey, title, parallel: true });
57
63
 
58
64
  if(filter) {
@@ -74,12 +80,6 @@ program
74
80
  }
75
81
  }
76
82
 
77
- if (!command.split) {
78
- process.exitCode = 255;
79
- console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
80
- return;
81
- }
82
-
83
83
  const testCmds = command.split(' ');
84
84
  console.log(APP_PREFIX, `🚀 Running`, chalk.green(command));
85
85
 
package/lib/config.js ADDED
@@ -0,0 +1,24 @@
1
+ // This file is used to read environment variables from .env file and process.env
2
+
3
+ // ! uncommenting next line leads ro reading vars from .env file
4
+ // require('dotenv').config();
5
+ const debug = require('debug')('@testomatio/reporter:config');
6
+
7
+ /* for possibility to use multiple env files (reading different paths)
8
+ const dotenv = require('dotenv');
9
+ const envFileVars = dotenv.config({ path: '.env' }).parsed; */
10
+
11
+ // select only TESTOMATIO related variables (only to print them in debug)
12
+ const testomatioEnvVars =
13
+ Object.keys(process.env)
14
+ .filter(key => key.startsWith('TESTOMATIO') || key.startsWith('S3_'))
15
+ .reduce((obj, key) => {
16
+ obj[key] = process.env[key];
17
+ return obj;
18
+ }, {}) || {};
19
+ debug('TESTOMATIO variables:', testomatioEnvVars);
20
+
21
+ // includes variables from .env file and process.env
22
+ const config = process.env;
23
+
24
+ module.exports = config;
package/lib/constants.js CHANGED
@@ -20,10 +20,17 @@ const STATUS = {
20
20
  SKIPPED: 'skipped',
21
21
  FINISHED: 'finished',
22
22
  };
23
+ // html pipe var
24
+ const HTML_REPORT = {
25
+ FOLDER: "html-report",
26
+ REPORT_DEFAULT_NAME: "testomatio-report.html",
27
+ TEMPLATE_NAME: 'testomatio.hbs'
28
+ };
23
29
 
24
30
  module.exports = {
25
31
  APP_PREFIX,
26
32
  TESTOMAT_TMP_STORAGE_DIR,
27
33
  CSV_HEADERS,
28
34
  STATUS,
35
+ HTML_REPORT
29
36
  }
@@ -3,10 +3,16 @@ const { S3 } = require('@aws-sdk/client-s3');
3
3
  const { Upload } = require('@aws-sdk/lib-storage');
4
4
 
5
5
  const fs = require('fs');
6
+ const util = require('util');
6
7
  const path = require('path');
8
+ const promiseRetry = require('promise-retry');
9
+
10
+ const readFile = util.promisify(fs.readFile);
11
+ const stat = util.promisify(fs.stat);
7
12
  const chalk = require('chalk');
8
13
  const { randomUUID } = require('crypto');
9
14
  const memoize = require('lodash.memoize');
15
+
10
16
  const { APP_PREFIX } = require('./constants');
11
17
 
12
18
  const keys = [
@@ -93,51 +99,64 @@ const uploadUsingS3 = async (filePath, runId) => {
93
99
  Key = filePath.name;
94
100
  }
95
101
 
96
- if (!fs.existsSync(filePath)) {
97
- console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
98
- return;
99
- }
100
-
101
102
  const {
102
- TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
103
+ TESTOMATIO_PRIVATE_ARTIFACTS,
104
+ S3_BUCKET
103
105
  } = getConfig();
104
106
 
105
- debug('S3 config', getMaskedConfig());
106
- debug('Uploading', filePath, 'to', S3_BUCKET);
107
-
108
- const fileData = fs.readFileSync(filePath);
107
+ try {
108
+ debug('S3 config', getMaskedConfig());
109
+ debug('Started upload', filePath, 'to ', S3_BUCKET);
109
110
 
110
- Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
111
- const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
111
+ // Verification that the file was actually created: 20 attempts of 0.5 second => 10sec
112
+ const isFileExist = await checkFileExists(filePath, 20, 500);
112
113
 
113
- const s3 = new S3(_getS3Config());
114
+ if (!isFileExist) {
115
+ console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
116
+ return;
117
+ }
114
118
 
115
- const params = {
116
- Bucket: S3_BUCKET,
117
- Key,
118
- Body: fileData,
119
- ContentType,
120
- ACL,
121
- };
119
+ debug('File: ', filePath, ' exists');
120
+
121
+ const fileData = await readFile(filePath);
122
122
 
123
- try {
123
+ Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
124
+
125
+ const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
126
+
127
+ if (!S3_BUCKET || !fileData) {
128
+ console.log(
129
+ APP_PREFIX,
130
+ chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
131
+ return;
132
+ }
133
+
134
+ const s3 = new S3(_getS3Config());
135
+
136
+ const params = {
137
+ Bucket: S3_BUCKET,
138
+ Key,
139
+ Body: fileData,
140
+ ContentType,
141
+ ACL,
142
+ };
143
+
124
144
  const out = new Upload({
125
145
  client: s3,
126
146
  params
127
147
  });
128
148
 
129
- await out.done();
130
- debug('Uploaded', out.singleUploadResult.Location)
131
-
132
- return out.singleUploadResult.Location;
133
- } catch (e) {
134
- console.log(e);
135
- console.log(APP_PREFIX, chalk.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
136
-
149
+ return await getS3LocationLink(out);
150
+ }
151
+ catch (e) {
152
+ debug('S3 file uploading error: ', e);
153
+
137
154
  console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
155
+
138
156
  if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
139
157
  console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
140
- } else {
158
+ }
159
+ else {
141
160
  console.log(
142
161
  APP_PREFIX,
143
162
  `To enable ${chalk.bold('PUBLIC')} uploads remove TESTOMATIO_PRIVATE_ARTIFACTS env variable`,
@@ -148,7 +167,6 @@ const fileData = fs.readFileSync(filePath);
148
167
  };
149
168
 
150
169
  const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
151
-
152
170
  const {
153
171
  S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
154
172
  } = getConfig();
@@ -158,6 +176,18 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
158
176
  const fileExtension = _getFileExtBase64(buffer.toString('base64'));
159
177
  const Key = `${runId}/${fileName}${fileExtension}`;
160
178
 
179
+ if (!S3_BUCKET || !buffer) {
180
+ console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
181
+ accessKeyId: S3_ACCESS_KEY_ID,
182
+ secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
183
+ region: S3_REGION,
184
+ bucket: S3_BUCKET,
185
+ acl: ACL,
186
+ endpoint: S3_ENDPOINT,
187
+ });
188
+ return;
189
+ }
190
+
161
191
  const s3 = new S3(_getS3Config());
162
192
 
163
193
  try {
@@ -171,20 +201,14 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
171
201
  ACL,
172
202
  }
173
203
  });
174
- await out.done();
175
204
 
176
- return out.singleUploadResult.Location;
177
- } catch (e) {
178
- console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
179
- accessKeyId: S3_ACCESS_KEY_ID,
180
- secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
181
- region: S3_REGION,
182
- bucket: S3_BUCKET,
183
- acl: ACL,
184
- endpoint: S3_ENDPOINT,
185
- });
205
+ return await getS3LocationLink(out);
206
+ }
207
+ catch (e) {
208
+ debug('S3 buffer uploading error: ', e);
186
209
 
187
210
  console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
211
+
188
212
  if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
189
213
  console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
190
214
  } else {
@@ -203,7 +227,9 @@ const uploadFileByPath = async (filePath, runId) => {
203
227
  return uploadUsingS3(filePath, runId);
204
228
  }
205
229
  } catch (e) {
206
- console.error(chalk.red('Error occurred while uploading artifacts'), e);
230
+ debug(e);
231
+
232
+ console.error(chalk.red('Error occurred while uploading artifacts! '), e);
207
233
  }
208
234
  };
209
235
 
@@ -213,10 +239,61 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
213
239
  return uploadUsingS3AsBuffer(buffer, fileName, runId);
214
240
  }
215
241
  } catch (e) {
216
- console.error(chalk.red('Error occurred while uploading artifacts'), e);
242
+ debug(e);
243
+
244
+ console.error(chalk.red('Error occurred while uploading artifacts! '), e);
217
245
  }
218
246
  };
219
247
 
248
+ const checkFileExists = async (filePath, attempts = 5, intervalMs = 500) => {
249
+ const checkFile = async () => {
250
+ const fileStats = await stat(filePath);
251
+ if (fileStats.isFile()) {
252
+ return true;
253
+ }
254
+
255
+ throw new Error('File not found');
256
+ };
257
+
258
+ try {
259
+ await promiseRetry(
260
+ {
261
+ retries: attempts,
262
+ minTimeout: intervalMs
263
+ },
264
+ checkFile
265
+ );
266
+
267
+ return true;
268
+ } catch (err) {
269
+ console.error(
270
+ chalk.yellow(`File ${filePath} was not found or did not have time to be generated...`)
271
+ );
272
+
273
+ return false;
274
+ }
275
+ };
276
+
277
+ const getS3LocationLink = async (out) => {
278
+ const response = await out.done();
279
+
280
+ let s3Location = response?.Location;
281
+
282
+ debug('Uploaded response.Location', s3Location);
283
+
284
+ if (!s3Location) {
285
+ // TODO: out: a fallback case - remove after deeper testing
286
+ s3Location = out?.singleUploadResult?.Location;
287
+ debug('Uploaded singleUploadResult.Location', s3Location);
288
+
289
+ if (!s3Location) {
290
+ throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
291
+ }
292
+ }
293
+
294
+ return s3Location;
295
+ };
296
+
220
297
  module.exports = {
221
298
  uploadFileByPath: memoize(uploadFileByPath),
222
299
  uploadFileAsBuffer: memoize(uploadFileAsBuffer),
@@ -23,7 +23,7 @@ class GitLabPipe {
23
23
  this.store = store;
24
24
  this.tests = [];
25
25
  // GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
26
- this.token = params.GITLAB_PAT || this.ENV.GITLAB_PAT;
26
+ this.token = params.GITLAB_PAT || process.env.GITLAB_PAT || this.ENV.GITLAB_PAT;
27
27
  this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
28
28
 
29
29
  debug(
@@ -0,0 +1,316 @@
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
+ const fileUrl = require('file-url');
8
+
9
+ const { fileSystem, isSameTest, ansiRegExp } = require('../utils/utils');
10
+ const { HTML_REPORT } = require('../constants');
11
+
12
+ class HtmlPipe {
13
+ constructor(params, store = {}) {
14
+ this.store = store || {};
15
+ this.title = params.title || process.env.TESTOMATIO_TITLE;
16
+ this.apiKey = params.apiKey || process.env.TESTOMATIO;
17
+ this.isHtml = process.env.TESTOMATIO_HTML_REPORT_SAVE;
18
+
19
+ debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
20
+
21
+ this.isEnabled = false;
22
+ this.htmlOutputPath = "";
23
+ this.fullHtmlOutputPath = "";
24
+ this.filenameMsg = "";
25
+ this.tests = [];
26
+
27
+ if (this.isHtml) {
28
+ this.isEnabled = true;
29
+ this.htmlReportDir = process.env.TESTOMATIO_HTML_REPORT_FOLDER || HTML_REPORT.FOLDER;
30
+
31
+ if (process.env.TESTOMATIO_HTML_FILENAME && process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
32
+ this.htmlReportName = process.env.TESTOMATIO_HTML_FILENAME
33
+ }
34
+
35
+ if (process.env.TESTOMATIO_HTML_FILENAME && !process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
36
+ this.htmlReportName = HTML_REPORT.REPORT_DEFAULT_NAME;
37
+ this.filenameMsg = "HTML filename must include the extension \".html\"." +
38
+ ` The default report name "${this.htmlReportDir}/${HTML_REPORT.REPORT_DEFAULT_NAME}" is used!`;
39
+ }
40
+
41
+ if (!process.env.TESTOMATIO_HTML_FILENAME) {
42
+ this.htmlReportName = HTML_REPORT.REPORT_DEFAULT_NAME;
43
+ }
44
+
45
+ this.templateFolderPath = path.resolve(__dirname, '..', 'template');
46
+ this.templateHtmlPath = path.resolve(this.templateFolderPath, HTML_REPORT.TEMPLATE_NAME);
47
+ this.htmlOutputPath = path.join(this.htmlReportDir, this.htmlReportName);
48
+ // create a new folder for the HTML reports
49
+ fileSystem.createDir(this.htmlReportDir);
50
+
51
+ debug(
52
+ chalk.yellow('HTML Pipe:'),
53
+ `Save HTML report: ${this.isEnabled}`,
54
+ `HTML report folder: ${this.htmlReportDir}, report name: ${this.htmlReportName}`
55
+ );
56
+ }
57
+ }
58
+
59
+ async createRun() {
60
+ // empty
61
+ }
62
+
63
+ updateRun() {
64
+ // empty
65
+ }
66
+
67
+ /**
68
+ * Add test data to the result array for saving. As a result of this function, we get a result object to save.
69
+ * @param {Object} test - object which includes each test entry.
70
+ */
71
+ addTest(test) {
72
+ if (!this.isEnabled) return;
73
+
74
+ if (!test.steps || !test.status) return;
75
+
76
+ const index = this.tests.findIndex(t => isSameTest(t, test));
77
+ // update if they were already added
78
+ if (index >= 0) {
79
+ this.tests[index] = merge(this.tests[index], test);
80
+ return;
81
+ }
82
+
83
+ this.tests.push(test);
84
+ }
85
+
86
+ async finishRun(runParams) {
87
+ if (!this.isEnabled) return;
88
+
89
+ if (this.isHtml) {
90
+ // GENERATE HTML reports based on the results data
91
+ this.buildReport({
92
+ runParams,
93
+ tests: this.tests,
94
+ outputPath: this.htmlOutputPath,
95
+ templatePath: this.templateHtmlPath,
96
+ warningMsg: this.filenameMsg
97
+ });
98
+ }
99
+ }
100
+ /**
101
+ * Generates an HTML report based on provided test data and a template.
102
+ * @param {object} opts - Test options used to generate the HTML report:
103
+ * runParams, tests, outputPath, templatePath
104
+ * @returns {void} - This function does not return anything.
105
+ */
106
+
107
+ buildReport(opts) {
108
+ const { runParams, tests, outputPath, templatePath, warningMsg: msg } = opts;
109
+
110
+ debug('HTML tests data:', tests);
111
+
112
+ if (!outputPath) {
113
+ console.log(chalk.yellow(`🚨 HTML export path is not set, ignoring...`));
114
+ return;
115
+ }
116
+
117
+ console.log(chalk.yellow(`⏳ The test results will be added to the HTML report. It will take some time...`));
118
+
119
+ if (msg) {
120
+ console.log(chalk.blue(msg));
121
+ }
122
+
123
+ tests.forEach(test => {
124
+
125
+ if (!test.message || test.message.trim() === "") {
126
+ test.message = "This test has no 'message' code";
127
+ }
128
+
129
+ if (!test.suite_title || test.suite_title.trim() === "") {
130
+ test.suite_title = "Unknown suite";
131
+ }
132
+
133
+ if (!test.title || test.title.trim() === "") {
134
+ test.title = "Unknown test title";
135
+ }
136
+
137
+ if (!test.files || test.files.length === 0) {
138
+ test.files = "This test has no files";
139
+ }
140
+
141
+ if (test.steps) {
142
+ if (!test.steps || test.steps.trim() === "") {
143
+ test.steps = "This test has no 'steps' code";
144
+ }
145
+ else {
146
+ test.steps = removeAnsiColorCodes(test.steps);
147
+ }
148
+ }
149
+
150
+ // TODO: u can added an additional test values to this checks in the future
151
+ });
152
+
153
+ const data = {
154
+ runId: this.store.runId || "",
155
+ status: runParams.status || "No status info",
156
+ parallel: runParams.isParallel || "No parallel info",
157
+ runUrl: this.store.runUrl || "",
158
+ executionTime: testExecutionSumTime(tests),
159
+ executionDate: getCurrentDateTimeFormatted(),
160
+ tests
161
+ };
162
+ // generate output HTML based on the template
163
+ const html = this.#generateHTMLReport(data, templatePath);
164
+
165
+ if (!html) return;
166
+
167
+ fs.writeFileSync(outputPath, html, 'utf-8');
168
+ // Check if the file exists
169
+ if (fs.existsSync(outputPath)) {
170
+ // Get the absolute path of the file
171
+ const absolutePath = path.resolve(outputPath);
172
+ // Convert the file path to a file URL
173
+ const fileUrlPath = fileUrl(absolutePath, {resolve: true});
174
+
175
+ debug('HTML tests data:', fileUrlPath);
176
+
177
+ console.log(chalk.green(`📊 The HTML report was successfully generated. Full filepath: ${fileUrlPath}`));
178
+ } else {
179
+ console.log(chalk.red(`🚨 Failed to generate the HTML report.`));
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Generates an HTML report based on provided test data and a template path.
185
+ * @param {any} data - Test data used to generate the HTML report.
186
+ * @param {string} [templatePath=""] - The path to the HTML template used for generating the report.
187
+ * @returns {string | void} - The generated HTML report as a string or void if templatePath is not provided.
188
+ */
189
+ #generateHTMLReport(data, templatePath = "") {
190
+ if (!templatePath) {
191
+ console.log(chalk.red(`🚨 HTML template not found. Report generation is impossible!`))
192
+ return;
193
+ }
194
+
195
+ const templateSource = fs.readFileSync(templatePath, 'utf8');
196
+ this.#loadReportHelpers();
197
+ try {
198
+ const template = handlebars.compile(templateSource);
199
+
200
+ return template(data);
201
+ }
202
+ catch (e) {
203
+ console.log('Unknown HTML report generation error: ', e);
204
+ }
205
+ }
206
+
207
+ #loadReportHelpers() {
208
+ handlebars.registerHelper('getTestsByStatus', (tests, status) =>
209
+ tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length
210
+ );
211
+
212
+ handlebars.registerHelper('json', (tests) => {
213
+ function replaceScriptTagsInArray(array) {
214
+ return array.map(obj => {
215
+ const keysToCheck = ["steps", "stack", "title", "suite_title", "message", "code"];
216
+ const newObj = {};
217
+
218
+ for (const key in obj) {
219
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
220
+ if (key === "example") {
221
+ newObj[key] = {};
222
+ for (const subKey in obj[key]) {
223
+ if (Object.prototype.hasOwnProperty.call(obj[key], subKey)) {
224
+ newObj[key][subKey] = typeof obj[key][subKey] === "string"
225
+ ? obj[key][subKey]
226
+ .replace(/<script>/g, "<$cript>")
227
+ .replace(/<\/script>/g, "</$cript>")
228
+ : obj[key][subKey];
229
+ }
230
+ }
231
+ } else if (keysToCheck.includes(key)) {
232
+ newObj[key] = typeof obj[key] === "string"
233
+ ? obj[key].replace(/<script>/g, "<$cript>").replace(/<\/script>/g, "</$cript>")
234
+ : obj[key];
235
+ } else {
236
+ newObj[key] = obj[key];
237
+ }
238
+ }
239
+ }
240
+
241
+ return newObj;
242
+ });
243
+ }
244
+
245
+ // Remove ANSI escape codes
246
+ return JSON.stringify(replaceScriptTagsInArray(tests));
247
+ });
248
+ }
249
+
250
+ toString() {
251
+ return 'HTML Reporter';
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Calculates the total execution time for an array of tests.
257
+ * @param {Object[]} tests - An array of test objects.
258
+ * @param {number} tests[].run_time - The execution time of each test in milliseconds.
259
+ * @returns {string} - The total execution time in a formatted duration string.
260
+ */
261
+ function testExecutionSumTime(tests) {
262
+ const totalMilliseconds = tests.reduce((sum, test) => {
263
+ if (typeof test.run_time === 'number') {
264
+ return sum + test.run_time;
265
+ }
266
+ return sum;
267
+ }, 0);
268
+
269
+ return formatDuration(totalMilliseconds);
270
+ }
271
+
272
+ /**
273
+ * Removes ANSI color codes and converts newline characters to HTML line breaks in a given string.
274
+ * @param {string} str - The input string containing ANSI color codes.
275
+ * @returns {string} - The updated string with removed ANSI color codes and replaced newline characters.
276
+ */
277
+ function removeAnsiColorCodes(str) {
278
+ let updatedStr = str.replace(ansiRegExp(), "");
279
+ updatedStr = updatedStr.replace(/\n/g, '<br>');
280
+
281
+ return updatedStr;
282
+ }
283
+
284
+ /**
285
+ * Formats duration in milliseconds into a human-readable string representation.
286
+ * @param {number} duration - The duration in milliseconds.
287
+ * @returns {string} - The formatted duration string (e.g., "2h 30m 15s 500ms").
288
+ */
289
+ function formatDuration(duration) {
290
+ const milliseconds = duration % 1000;
291
+ duration = (duration - milliseconds) / 1000;
292
+ const seconds = duration % 60;
293
+ duration = (duration - seconds) / 60;
294
+ const minutes = duration % 60;
295
+ const hours = (duration - minutes) / 60;
296
+
297
+ return `${hours}h ${minutes}m ${seconds}s ${milliseconds}ms`;
298
+ }
299
+
300
+ /**
301
+ * Retrieves the current date and time in a formatted string.
302
+ * @returns {string} - The formatted date and time string (e.g., "(01/01/2023 12:00:00)").
303
+ */
304
+ function getCurrentDateTimeFormatted() {
305
+ const currentDate = new Date();
306
+ const day = currentDate.getDate().toString().padStart(2, '0');
307
+ const month = (currentDate.getMonth() + 1).toString().padStart(2, '0');
308
+ const year = currentDate.getFullYear();
309
+ const hours = currentDate.getHours().toString().padStart(2, '0');
310
+ const minutes = currentDate.getMinutes().toString().padStart(2, '0');
311
+ const seconds = currentDate.getSeconds().toString().padStart(2, '0');
312
+
313
+ return `(${day}/${month}/${year} ${hours}:${minutes}:${seconds})`;
314
+ }
315
+
316
+ module.exports = HtmlPipe;