@testomatio/reporter 1.2.0-beta-3 → 1.2.0-beta-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/lib/pipe/html.js CHANGED
@@ -15,18 +15,14 @@ class HtmlPipe {
15
15
  this.title = params.title || process.env.TESTOMATIO_TITLE;
16
16
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
17
17
  this.isHtml = process.env.TESTOMATIO_HTML_REPORT_SAVE;
18
- this.data = {};
19
18
 
20
- debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
21
- if (!this.apiKey) {
22
- return;
23
- }
19
+ debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
24
20
 
25
21
  this.isEnabled = false;
26
22
  this.htmlOutputPath = "";
27
23
  this.fullHtmlOutputPath = "";
24
+ this.filenameMsg = "";
28
25
  this.tests = [];
29
- this.data = {};
30
26
 
31
27
  if (this.isHtml) {
32
28
  this.isEnabled = true;
@@ -38,6 +34,8 @@ class HtmlPipe {
38
34
 
39
35
  if (process.env.TESTOMATIO_HTML_FILENAME && !process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
40
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!`;
41
39
  }
42
40
 
43
41
  if (!process.env.TESTOMATIO_HTML_FILENAME) {
@@ -89,77 +87,88 @@ class HtmlPipe {
89
87
  if (!this.isEnabled) return;
90
88
 
91
89
  if (this.isHtml) {
92
-
93
- this.tests.forEach(test => {
94
-
95
- if (!test.message || test.message.trim() === "") {
96
- test.message = "This test has no 'message' code";
97
- }
98
-
99
- if (!test.suite_title || test.suite_title.trim() === "") {
100
- test.suite_title = "Unknown suite";
101
- }
102
-
103
- if (!test.title || test.title.trim() === "") {
104
- test.title = "Unknown test title";
105
- }
106
-
107
- if (!test.files || test.files.length === 0) {
108
- test.files = "This test has no files";
109
- }
110
-
111
- if (test.steps) {
112
- if (!test.steps || test.steps.trim() === "") {
113
- test.steps = "This test has no 'steps' code";
114
- }
115
- else {
116
- test.steps = this.#removeAnsiColorCodes(test.steps);
117
- }
118
- }
119
-
120
- // TODO: u can added an additional test values to this checks in the future
121
- });
122
-
123
- this.data = {
124
- runId: this.store.runId,
125
- status: runParams.status,
126
- parallel: runParams.isParallel,
127
- runUrl: this.store.runUrl,
128
- executionTime: testExecutionSumTime(this.tests),
129
- executionDate: getCurrentDateTimeFormatted(),
130
- tests: this.tests
131
- };
132
-
133
90
  // GENERATE HTML reports based on the results data
134
- this.buildReport(this.data);
91
+ this.buildReport({
92
+ runParams,
93
+ tests: this.tests,
94
+ outputPath: this.htmlOutputPath,
95
+ templatePath: this.templateHtmlPath,
96
+ warningMsg: this.filenameMsg
97
+ });
135
98
  }
136
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
+ */
137
106
 
138
- buildReport(data) {
139
- debug('HTML tests data:', data);
107
+ buildReport(opts) {
108
+ const { runParams, tests, outputPath, templatePath, warningMsg: msg } = opts;
109
+
110
+ debug('HTML tests data:', tests);
140
111
 
141
- if (!this.htmlOutputPath && this.htmlOutputPath !== "") {
112
+ if (!outputPath) {
142
113
  console.log(chalk.yellow(`🚨 HTML export path is not set, ignoring...`));
143
114
  return;
144
115
  }
145
116
 
146
117
  console.log(chalk.yellow(`⏳ The test results will be added to the HTML report. It will take some time...`));
147
- // generate output HTML based on the template
148
- const html = this.#generateHTMLReport(data);
149
118
 
150
- if (process.env.TESTOMATIO_HTML_FILENAME && !process.env.TESTOMATIO_HTML_FILENAME.endsWith(".html")) {
151
- console.log(
152
- chalk.blue("HTML filename must include the extension \".html\"." +
153
- ` The default report name "${this.htmlOutputPath}" is used!`)
154
- );
119
+ if (msg) {
120
+ console.log(chalk.blue(msg));
155
121
  }
156
122
 
157
- fs.writeFileSync(this.htmlOutputPath, html, 'utf-8');
123
+ tests.forEach(test => {
124
+
125
+ if (!test.message || test.message.trim() === "") {
126
+ test.message = "This test has no 'message' code";
127
+ }
158
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');
159
168
  // Check if the file exists
160
- if (fs.existsSync(this.htmlOutputPath)) {
169
+ if (fs.existsSync(outputPath)) {
161
170
  // Get the absolute path of the file
162
- const absolutePath = path.resolve(this.htmlOutputPath);
171
+ const absolutePath = path.resolve(outputPath);
163
172
  // Convert the file path to a file URL
164
173
  const fileUrlPath = fileUrl(absolutePath, {resolve: true});
165
174
 
@@ -171,13 +180,19 @@ class HtmlPipe {
171
180
  }
172
181
  }
173
182
 
174
- #generateHTMLReport(data) {
175
- if (!this.templateHtmlPath) {
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) {
176
191
  console.log(chalk.red(`🚨 HTML template not found. Report generation is impossible!`))
177
192
  return;
178
193
  }
179
194
 
180
- const templateSource = fs.readFileSync(this.templateHtmlPath, 'utf8');
195
+ const templateSource = fs.readFileSync(templatePath, 'utf8');
181
196
  this.#loadReportHelpers();
182
197
  try {
183
198
  const template = handlebars.compile(templateSource);
@@ -232,18 +247,17 @@ class HtmlPipe {
232
247
  });
233
248
  }
234
249
 
235
- #removeAnsiColorCodes(str) {
236
- let updatedStr = str.replace(ansiRegExp(), "");
237
- updatedStr = updatedStr.replace(/\n/g, '<br>');
238
-
239
- return updatedStr;
240
- }
241
-
242
250
  toString() {
243
251
  return 'HTML Reporter';
244
252
  }
245
253
  }
246
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
+ */
247
261
  function testExecutionSumTime(tests) {
248
262
  const totalMilliseconds = tests.reduce((sum, test) => {
249
263
  if (typeof test.run_time === 'number') {
@@ -255,6 +269,23 @@ function testExecutionSumTime(tests) {
255
269
  return formatDuration(totalMilliseconds);
256
270
  }
257
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
+ */
258
289
  function formatDuration(duration) {
259
290
  const milliseconds = duration % 1000;
260
291
  duration = (duration - milliseconds) / 1000;
@@ -266,6 +297,10 @@ function formatDuration(duration) {
266
297
  return `${hours}h ${minutes}m ${seconds}s ${milliseconds}ms`;
267
298
  }
268
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
+ */
269
304
  function getCurrentDateTimeFormatted() {
270
305
  const currentDate = new Date();
271
306
  const day = currentDate.getDate().toString().padStart(2, '0');
@@ -7,7 +7,11 @@
7
7
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
9
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer">
10
- <title>Report #{{runId}} - Testomat.io</title>
10
+ {{#if runId}}
11
+ <title>Report #{{runId}} - Testomat.io</title>
12
+ {{else}}
13
+ <title>Report Testomat.io</title>
14
+ {{/if}}
11
15
  <style>
12
16
  body {
13
17
  padding: 0;
@@ -32,7 +36,7 @@
32
36
  }
33
37
  p, span{
34
38
  font-weight: 400;
35
- font-size: 12px;
39
+ font-size: 14px;
36
40
  color: grey;
37
41
  margin: 0;
38
42
  }
@@ -245,6 +249,10 @@
245
249
  font-size: 14px;
246
250
  white-space: pre-line;
247
251
  }
252
+ .test__empty__list {
253
+ margin-bottom: 10px;
254
+ color: grey;
255
+ }
248
256
  /* Passed TAB*/
249
257
  #passedTest:not(:checked) + .btn-outline-dark {
250
258
  background-color: #39BD2F;
@@ -279,19 +287,27 @@
279
287
  <div class="col-12">
280
288
  <div class="header">
281
289
  <div class="header__block">
282
- <div class="header__case">
283
- <p>Run</p>
284
- <span><strong>#</strong>{{runId}}</span>
285
- </div>
290
+ {{#if runId}}
291
+ <div class="header__case">
292
+ <p>Run</p>
293
+ <span><strong>#</strong>{{runId}}</span>
294
+ </div>
295
+ {{/if}}
286
296
  <p class="header__type">
287
297
  <i class="fa-solid fa-face-smile"></i>
288
298
  automated job
289
299
  </p>
290
300
  </div>
291
301
  <div class="header__block">
292
- <a href="{{runUrl}}" target="_blank" class="btn btn-primary">
293
- Full Report
294
- </a>
302
+ {{#if runUrl}}
303
+ <a href="{{runUrl}}" target="_blank" class="btn btn-primary">
304
+ Full Report
305
+ </a>
306
+ {{else}}
307
+ <button class="btn btn-primary" disabled>
308
+ Full Report
309
+ </button>
310
+ {{/if}}
295
311
  </div>
296
312
  </div>
297
313
  </div>
@@ -389,72 +405,78 @@
389
405
 
390
406
  <div class="row level_5">
391
407
  <div class="col-12">
408
+ {{#if tests.length}}
409
+ <div class="testWrapp">
410
+ {{#each tests}}
392
411
 
393
- <div class="testWrapp">
394
- {{#each tests}}
412
+ <div class="testitem d-none" name="testitem" type="dummy" category="false">
395
413
 
396
- <div class="testitem d-none" name="testitem" type="dummy" category="false">
397
-
398
- <div class="testitem__top">
399
- <div class="testitem__icon">
400
- <i class="fa-solid fa-chevron-right testitem__ico testitem__ico_right"></i>
401
- <i class="fa-solid fa-chevron-down d-none testitem__ico testitem__ico_down"></i>
414
+ <div class="testitem__top">
415
+ <div class="testitem__icon">
416
+ <i class="fa-solid fa-chevron-right testitem__ico testitem__ico_right"></i>
417
+ <i class="fa-solid fa-chevron-down d-none testitem__ico testitem__ico_down"></i>
418
+ </div>
419
+ <p class="testitem__name">Test</p>
402
420
  </div>
403
- <p class="testitem__name">Test</p>
404
- </div>
405
421
 
406
- <div class="testitem__body d-none">
422
+ <div class="testitem__body d-none">
407
423
 
408
- <div class="testitem__menu">
409
- <span type="steps" class="testitem__mitem testitem__mitem_active">Steps <i class="fa-solid fa-arrow-right"></i></span>
410
- <span type="status" class="testitem__mitem">Status <i class="fa-solid fa-arrow-right"></i></span>
411
- <span type="message" class="testitem__mitem">Message <i class="fa-solid fa-arrow-right"></i></span>
412
- <span type="files" class="testitem__mitem">Files <i class="fa-solid fa-arrow-right"></i></span>
413
- </div>
424
+ <div class="testitem__menu">
425
+ <span type="steps" class="testitem__mitem testitem__mitem_active">Steps <i class="fa-solid fa-arrow-right"></i></span>
426
+ <span type="status" class="testitem__mitem">Status <i class="fa-solid fa-arrow-right"></i></span>
427
+ <span type="message" class="testitem__mitem">Message <i class="fa-solid fa-arrow-right"></i></span>
428
+ <span type="files" class="testitem__mitem">Files <i class="fa-solid fa-arrow-right"></i></span>
429
+ </div>
414
430
 
415
- <div class="testitem__content">
431
+ <div class="testitem__content">
416
432
 
417
- <!-- 1 -->
418
- <div class="testitem__case" type="steps">
419
- <p class="testitem__title">Steps</p>
420
- <div class="testitem__block">
421
- <span>...</span>
433
+ <!-- 1 -->
434
+ <div class="testitem__case" type="steps">
435
+ <p class="testitem__title">Steps</p>
436
+ <div class="testitem__block">
437
+ <span>...</span>
438
+ </div>
422
439
  </div>
423
- </div>
424
- <!-- 1 -->
425
-
426
- <!-- 2 -->
427
- <div class="testitem__case d-none" type="status">
428
- <p class="testitem__title">Status</p>
429
- <div class="testitem__block">
430
- <span>...</span>
440
+ <!-- 1 -->
441
+
442
+ <!-- 2 -->
443
+ <div class="testitem__case d-none" type="status">
444
+ <p class="testitem__title">Status</p>
445
+ <div class="testitem__block">
446
+ <span>...</span>
447
+ </div>
431
448
  </div>
432
- </div>
433
- <!-- 2 -->
434
-
435
- <!-- 3 -->
436
- <div class="testitem__case d-none" type="message">
437
- <p class="testitem__title">Message</p>
438
- <div class="testitem__block">
439
- <span>...</span>
449
+ <!-- 2 -->
450
+
451
+ <!-- 3 -->
452
+ <div class="testitem__case d-none" type="message">
453
+ <p class="testitem__title">Message</p>
454
+ <div class="testitem__block">
455
+ <span>...</span>
456
+ </div>
440
457
  </div>
441
- </div>
442
- <!-- 3 -->
443
-
444
- <!-- 4 -->
445
- <div class="testitem__case d-none" type="files">
446
- <p class="testitem__title">Files</p>
447
- <div class="testitem__block">
448
- <span>...</span>
458
+ <!-- 3 -->
459
+
460
+ <!-- 4 -->
461
+ <div class="testitem__case d-none" type="files">
462
+ <p class="testitem__title">Files</p>
463
+ <div class="testitem__block">
464
+ <span>...</span>
465
+ </div>
449
466
  </div>
467
+ <!-- 4 -->
450
468
  </div>
451
- <!-- 4 -->
452
469
  </div>
453
470
  </div>
471
+ {{/each}}
472
+ </div>
473
+ {{else}}
474
+ <div class="row">
475
+ <div class="col-12 test__empty__list">
476
+ <p>No tests found to display 😔</p>
454
477
  </div>
455
- {{/each}}
456
- </div>
457
-
478
+ </div>
479
+ {{/if}}
458
480
  </div>
459
481
  </div>
460
482
 
@@ -469,23 +491,33 @@
469
491
 
470
492
  // Draw the chart and set the chart values
471
493
  function drawChart() {
494
+ let data = {};
472
495
  const passedTests = {{getTestsByStatus tests "PASSED"}};
473
496
  const failedTests = {{getTestsByStatus tests "FAILED"}};
474
497
  const skippedTests = {{getTestsByStatus tests "SKIPPED"}};
475
498
 
476
- const data = google.visualization.arrayToDataTable([
499
+ if (passedTests === 0 && failedTests === 0 && skippedTests === 0) {
500
+ data = google.visualization.arrayToDataTable([
501
+ ['Task', 'Tests'],
502
+ ['No Tests', 1]
503
+ ])
504
+ }
505
+ else {
506
+ data = google.visualization.arrayToDataTable([
477
507
  ['Task', 'Tests'],
508
+ ['No Tests', 0],
478
509
  ['Passed', passedTests],
479
510
  ['Failed', failedTests],
480
511
  ['Skipped', skippedTests],
481
512
  ]);
513
+ }
482
514
 
483
515
  // Optional: add a title and set the width and height of the chart
484
516
  const options = {
485
517
  'title':'',
486
518
  'width':550,
487
519
  'height':300,
488
- 'colors': ['#39BD2F', '#F2230C', '#F2C00C']
520
+ 'colors': ['#939992', '#39BD2F', '#F2230C', '#F2C00C']
489
521
  };
490
522
 
491
523
  // Display the chart inside the <div> element with id="piechart"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.2.0-beta-3",
3
+ "version": "1.2.0-beta-4",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -66,6 +66,7 @@
66
66
  "eslint-plugin-import": "^2.25.4",
67
67
  "jasmine": "^3.10.0",
68
68
  "jest": "^27.4.7",
69
+ "jsdom": "^22.1.0",
69
70
  "mocha": "^9.2.0",
70
71
  "mock-http-server": "^1.4.5",
71
72
  "pino": "^8.15.0",
@@ -1,47 +0,0 @@
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
- }