@testomatio/reporter 1.2.1-beta → 1.2.1-beta.codecept-id

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.
Files changed (47) hide show
  1. package/README.md +61 -54
  2. package/lib/adapter/codecept.js +136 -57
  3. package/lib/adapter/cucumber/current.js +103 -60
  4. package/lib/adapter/cucumber/legacy.js +27 -12
  5. package/lib/adapter/cucumber.js +2 -2
  6. package/lib/adapter/cypress-plugin/index.js +52 -25
  7. package/lib/adapter/jasmine.js +1 -1
  8. package/lib/adapter/jest.js +49 -11
  9. package/lib/adapter/mocha.js +103 -51
  10. package/lib/adapter/playwright.js +100 -31
  11. package/lib/adapter/webdriver.js +1 -1
  12. package/lib/bin/reportXml.js +14 -13
  13. package/lib/bin/startTest.js +27 -6
  14. package/lib/client.js +193 -69
  15. package/lib/config.js +34 -0
  16. package/lib/constants.js +19 -7
  17. package/lib/data-storage.js +203 -0
  18. package/lib/fileUploader.js +128 -53
  19. package/lib/junit-adapter/adapter.js +0 -2
  20. package/lib/junit-adapter/csharp.js +3 -4
  21. package/lib/junit-adapter/index.js +3 -3
  22. package/lib/junit-adapter/java.js +35 -17
  23. package/lib/junit-adapter/javascript.js +1 -2
  24. package/lib/junit-adapter/python.js +12 -14
  25. package/lib/junit-adapter/ruby.js +1 -2
  26. package/lib/pipe/csv.js +5 -3
  27. package/lib/pipe/github.js +27 -39
  28. package/lib/pipe/gitlab.js +20 -24
  29. package/lib/pipe/html.js +317 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +182 -55
  32. package/lib/reporter-functions.js +46 -0
  33. package/lib/reporter.js +11 -9
  34. package/lib/services/artifacts.js +57 -0
  35. package/lib/services/index.js +13 -0
  36. package/lib/services/key-values.js +58 -0
  37. package/lib/services/logger.js +311 -0
  38. package/lib/template/template-draft.hbs +249 -0
  39. package/lib/template/testomatio.hbs +388 -0
  40. package/lib/utils/pipe_utils.js +128 -0
  41. package/lib/{util.js → utils/utils.js} +145 -12
  42. package/lib/xmlReader.js +211 -122
  43. package/package.json +18 -8
  44. package/lib/_ArtifactStorageOld.js +0 -142
  45. package/lib/artifactStorage.js +0 -25
  46. package/lib/dataStorage.js +0 -180
  47. package/lib/logger.js +0 -278
@@ -0,0 +1,311 @@
1
+ const chalk = require('chalk');
2
+ const debug = require('debug')('@testomatio/reporter:services-logger');
3
+ const { dataStorage } = require('../data-storage');
4
+
5
+ const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
6
+ const LEVELS = {
7
+ ALL: { severity: 1, color: '' },
8
+ VERBOSE: { severity: 3, color: 'grey' },
9
+ TRACE: { severity: 5, color: 'grey' },
10
+ DEBUG: { severity: 7, color: 'cyan' },
11
+ INFO: { severity: 9, color: 'black' },
12
+ LOG: { severity: 11, color: 'black' },
13
+ WARN: { severity: 13, color: 'yellow' },
14
+ ERROR: { severity: 15, color: 'red' },
15
+ };
16
+
17
+ // ! DON'T use console.log, console.warn, etc in this file, because it will lead to infinite loop
18
+ // use debug() instead
19
+
20
+ /**
21
+ * Logger allows to intercept logs from any logger (console.log, tracer, pino, etc)
22
+ * and save in the testomatio reporter.
23
+ * Supports different syntaxes to satisfy any user preferences.
24
+ */
25
+ class Logger {
26
+ // set default logger to be used in log, warn, error, etc methods
27
+ #originalUserLogger = { ...console };
28
+
29
+ #userLoggerWithOverridenMethods;
30
+
31
+ static #instance;
32
+
33
+ /**
34
+ *
35
+ * @returns {Logger}
36
+ */
37
+ static getInstance() {
38
+ if (!this.#instance) {
39
+ this.#instance = new Logger();
40
+ }
41
+ return this.#instance;
42
+ }
43
+
44
+ logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
45
+
46
+ constructor() {
47
+ if (!dataStorage.isFileStorage || process.env.TESTOMATIO_INTERCEPT_CONSOLE_LOGS) this.intercept(console);
48
+ }
49
+
50
+ /**
51
+ * Allows you to define a step inside a test. Step name is attached to the report and
52
+ * helps to understand the test flow.
53
+ * @param {*} strings
54
+ * @param {...any} values
55
+ */
56
+ step(strings, ...values) {
57
+ let logs = '';
58
+ for (let i = 0; i < strings.length; i++) {
59
+ logs += strings[i];
60
+ if (i < values.length) {
61
+ logs += values[i];
62
+ }
63
+ }
64
+ logs = chalk.blue(`> ${logs}`);
65
+ dataStorage.putData('log', logs);
66
+ }
67
+
68
+ /**
69
+ *
70
+ * @param {string} context testId or test context from test runner
71
+ * @returns {string[]}
72
+ */
73
+ getLogs(context) {
74
+ const logs = dataStorage.getData('log', context);
75
+ if (!logs) return [];
76
+ return logs;
77
+ }
78
+
79
+ #stringifyLogs(...args) {
80
+ const logs = [];
81
+ // stringify everything except strings
82
+ for (const arg of args) {
83
+ // ignore empty strings
84
+ if (arg === '') continue;
85
+ if (typeof arg === 'string') {
86
+ logs.push(arg);
87
+ } else if (Array.isArray(arg)) {
88
+ logs.push(arg.join(' '));
89
+ } else {
90
+ try {
91
+ // eslint-disable-next-line no-unused-expressions
92
+ this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
93
+ } catch (e) {
94
+ debug('Error while stringify object', e);
95
+ logs.push(arg);
96
+ }
97
+ }
98
+ }
99
+ return logs.join(' ');
100
+ }
101
+
102
+ /**
103
+ * Tagget template literal. Allows to use different syntaxes:
104
+ * 1. Tagget template: log`text ${someVar}`
105
+ * 2. Standard: log(`text ${someVar}`)
106
+ * 3. Standard with multiple arguments: log('text', someVar)
107
+ */
108
+ _templateLiteralLog(strings, ...args) {
109
+ if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
110
+ if (Array.isArray(args)) args = args.filter(item => item !== '');
111
+
112
+ let logs;
113
+ // this block means tagged template is used (syntax like $`text ${someVar}`)
114
+ if (Array.isArray(strings) && strings.length === args.length + 1) {
115
+ logs = strings.reduce(
116
+ (result, current, index) =>
117
+ result +
118
+ current +
119
+ // strings are splitted by args when use tagged template, thus we add arg after each string
120
+ // it looks like: `string1 arg1 string2 arg2 string3`
121
+ (args[index] !== undefined // eslint-disable-line no-nested-ternary
122
+ ? typeof args[index] === 'string'
123
+ ? args[index] // add arg as it is
124
+ : this.#stringifyLogs(args[index]) // stringify arg
125
+ : ''),
126
+ // initial accumulator value
127
+ '',
128
+ );
129
+ } else {
130
+ // this block means arguments syntax is used (syntax like $('text', someVar))
131
+ // in this case strings represents just a first argument
132
+ logs = this.#stringifyLogs(strings, ...args);
133
+ }
134
+ this.#originalUserLogger.log(logs);
135
+ dataStorage.putData('log', logs);
136
+ }
137
+
138
+ /**
139
+ * This function is a wrapper for each logging methods (log, warn, error etc) (not to repeat the same code)
140
+ * @param {*} argsArray
141
+ * @param {*} level
142
+ * @returns
143
+ */
144
+ #logWrapper(argsArray, level) {
145
+ if (!argsArray.length) return;
146
+
147
+ const severity = LEVELS[level].severity;
148
+ if (severity < LEVELS[this.logLevel]?.severity) return;
149
+
150
+ const logs = this.#stringifyLogs(...argsArray);
151
+
152
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
153
+ // do not attach logs from testomatio reporter itself
154
+ if (!logs.includes('[TESTOMATIO]')) {
155
+ dataStorage.putData('log', colorizedLogs);
156
+ }
157
+
158
+ try {
159
+ // level.toLowerCase() represents method name (log, warn, error, etc)
160
+ this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
161
+ } catch (e) {
162
+ // method could be unexisting, ignore error
163
+ }
164
+ }
165
+
166
+ assert(...args) {
167
+ this.#logWrapper(args, 'ERROR');
168
+ }
169
+
170
+ debug(...args) {
171
+ this.#logWrapper(args, 'DEBUG');
172
+ }
173
+
174
+ error(...args) {
175
+ this.#logWrapper(args, 'ERROR');
176
+ }
177
+
178
+ info(...args) {
179
+ this.#logWrapper(args, 'INFO');
180
+ }
181
+
182
+ log(...args) {
183
+ this.#logWrapper(args, 'LOG');
184
+ }
185
+
186
+ trace(...args) {
187
+ this.#logWrapper(args, 'TRACE');
188
+ }
189
+
190
+ warn(...args) {
191
+ this.#logWrapper(args, 'WARN');
192
+ }
193
+
194
+ /**
195
+ * Intercepts user logger messages.
196
+ * When call this method, Logger start to control the user logger
197
+ * @param {*} userLogger
198
+ */
199
+ intercept(userLogger) {
200
+ // STEP 1: reset previously intercepted logger methods to original
201
+ if (this.#userLoggerWithOverridenMethods) {
202
+ for (const method of LOG_METHODS) {
203
+ this.#userLoggerWithOverridenMethods[method] = this.#originalUserLogger[method];
204
+ }
205
+ }
206
+
207
+ // STEP 2: intercept new logger
208
+ this.#originalUserLogger = { ...userLogger };
209
+
210
+ const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
211
+ debug(`Intercepting ${isUserLoggerConsole ? 'console' : 'some user'} logger}`);
212
+
213
+ // override user logger (any, e.g. console) methods to intercept log messages
214
+ for (const method of LOG_METHODS) {
215
+ /*
216
+ its better to create method even if it does not exist in user logger;
217
+ on method invocation, we will store the data anyway and catch block will prevent potential errors
218
+ while trying to output the message to terminal
219
+ */
220
+ // if (!this._loggerToIntercept[method]) continue;
221
+ userLogger[method] = (...args) => this[method](...args);
222
+ }
223
+
224
+ this.#userLoggerWithOverridenMethods = userLogger;
225
+
226
+ /*
227
+ Initial idea was to intercept any logger (tracer, pino, etc),
228
+ intercept message and provide output by the same logger.
229
+ But reality brings some problems: the same messages are intercepted multiple times
230
+ (because of multiple loggers are created at the same terminal process).
231
+ Also its difficult to understand (actually did not find the way to do it) if logger was already intercepted or not.
232
+ Thus, decided to intercept only console by default and provide output by default console.
233
+ It means, if user uses his own logger, its messages will be intercepted,
234
+ but the output will be always provided by console.
235
+ TODO: try to implement the providing output to terminal by user logger
236
+ */
237
+ }
238
+
239
+ stopInterception() {
240
+ debug('Stop ntercepting logs');
241
+
242
+ // restore original user logger
243
+ if (this.#userLoggerWithOverridenMethods) {
244
+ for (const method of LOG_METHODS) {
245
+ this.#userLoggerWithOverridenMethods[method] = this.#originalUserLogger[method];
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Allows to configure logger. Make sure you do it before the logger usage in your code.
252
+ *
253
+ * @param {Object} [config={}] - The configuration object.
254
+ * @param {string} [config.logLevel] - The desired log level. Valid values are 'DEBUG', 'INFO', 'WARN', and 'ERROR'.
255
+ * @param {boolean} [config.prettyObjects] - Specifies whether to enable pretty printing of objects.
256
+ * @returns {void}
257
+ */
258
+ configure(config = {}) {
259
+ if (!config) return;
260
+ if (config.prettyObjects === false || config.prettyObjects === true) this.prettyObjects = config.prettyObjects;
261
+ if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
262
+ }
263
+ }
264
+
265
+ module.exports.logger = Logger.getInstance();
266
+
267
+ // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
268
+ // upd: did not face such loggers, but still could be useful
269
+
270
+ /* Cypress
271
+ There is no listener like "after:test" in cypress, only "after:spec" is available.
272
+ Thus, cannot separate logs even when I gather them (because I don't know when the test is done, just know about suite).
273
+ Also there is no easy way to access the message from cy.log() function.
274
+ (Using testomatio logger – logger.log() is not convenient because Cypress chains its commands,
275
+ thus such command will interrupt the chain.)
276
+
277
+ (Could not implement intercepting of cy.log('message'));
278
+ I found the only ability to get any logs using .task('log', 'message') (this is custom, not default cypress command)
279
+ and then intercept it with:
280
+ on('task', {
281
+ log (message) {
282
+ return null
283
+ }
284
+ })
285
+
286
+ but:
287
+ 1) it does not solve problem with getting current running testId;
288
+ 2) leads to warning "Warning: Multiple attempts to register the following task(s):".)
289
+
290
+ My way to get test id:
291
+ add cypress command to save test title to file)):
292
+ Cypress.Commands.add('writeTestTitleToFile', () => {
293
+ const testTitle = cy.state('runnable').title;
294
+ cy.writeFile('testomatio_test_title', testTitle);
295
+ });
296
+
297
+ Finally, in the test it will look like:
298
+ cy
299
+ .writeTestTitleToFile() // <<<
300
+ .task('log', 'This is a log message from the test') // <<<
301
+
302
+ .get('element)
303
+ .type('text')
304
+ .click()
305
+
306
+
307
+ Parallelization in Cypress is only available if using Cypress Dashboard??
308
+ */
309
+
310
+ // TODO: add time to logs
311
+ // TODO: add logger name to logs?
@@ -0,0 +1,249 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet">
7
+ <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/accordionjs@2.1.2/accordion.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
10
+ <title>Report {{runId}} - Testomat.io</title>
11
+ <link rel="stylesheet" href="./style.css">
12
+ {{!-- TODO: styling create as a new additional file instead of template => copy default one --}}
13
+ <style>
14
+ body {
15
+ font-family: "Fira Sans", sans-serif;
16
+ }
17
+ </style>
18
+ </head>
19
+
20
+ <body class="bg-gray-50 pt-6 px-4 lg:pt-4 lg:px-40 h-full;">
21
+ <div id="testomatio_report">
22
+ <div class="flex items-center flex-col space-y-4 md:flex-row md:justify-between md:h-14 mb-6">
23
+ <div class="flex flex-col md:flex-row space-x-3 items-center">
24
+ <h2 class="text-gray-900 leading-none text-3xl font-semibold mb-0">{{runId}}
25
+ </h2>
26
+ {{!-- <span class="text-sm text-gray-500">
27
+ {{executionDate}}
28
+ </span> --}}
29
+ </div>
30
+ <div class="flex space-x-3 items-center">
31
+ <a class="inline-flex items-center border border-transparent leading-none text-white bg-indigo-500 hover:bg-indigo-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer px-4 py-2 text-base leading-none"
32
+ style="min-height: 38px"
33
+ href="{{runUrl}}"
34
+ target="_blank">
35
+ Full Report
36
+ </a>
37
+ </div>
38
+ </div>
39
+ <div class="bg-white border overflow-hidden rounded-lg divide-y divide-gray-200 mb-12">
40
+ <div class="flex flex-col lg:divide-x divide-gray-200 sm:flex-col md:flex-col lg:flex-row">
41
+ <div class="w-full lg:w-1/2">
42
+ <!-- Left chart section -->
43
+ <div class="p-4">
44
+ <h2 class="text-xl font-semibold mb-4">Testomatio Test Results</h2>
45
+ <span class="text-sm text-gray-500 items-center">{{executionDate}}</span>
46
+ <div class="hart-container" style="height: 200px;">
47
+ <canvas id="testomatio_chart"></canvas>
48
+ <div for="testomatio_chart"></div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="w-full lg:w-1/2">
53
+ <!-- Right chart section -->
54
+ <div class="p-4">
55
+ <h2 class="text-xl font-semibold mb-4">Testomatio Run Info</h2>
56
+ <dl>
57
+ <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
58
+ <dt class="text-sm font-medium text-gray-500">
59
+ RUN Status
60
+ </dt>
61
+ <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
62
+ <span class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-bold leading-none bg-green-200 text-green-600 rounded-full">
63
+ {{status}}
64
+ </span>
65
+ </dd>
66
+ </div>
67
+ <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
68
+ <dt class="text-sm font-medium text-gray-500">
69
+ Duration
70
+ </dt>
71
+ <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
72
+ <span>
73
+ <i class="fa fa-clock-o"></i>
74
+ </span>
75
+ {{executionTime}}
76
+ </dd>
77
+ </div>
78
+ </div>
79
+ </dl>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="px-4 py-5 lg:px-20 lg:py-5">
86
+ <!-- TODO: maybe add a search section by test NAME!! -->
87
+ <div class="px-4 py-5 lg:px-20 lg:py-5 border rounded-md mb-4">
88
+ <div class="mb-2">
89
+ <h3 class="text-2xl leading-10 font-medium text-gray-900">
90
+ Search and Summary
91
+ </h3>
92
+ </div>
93
+ <div class="flex items-center space-x-4">
94
+ <div class="relative">
95
+ <input type="text" id="search-input" placeholder="Search by test title..." class="border rounded-md py-1 px-2 w-48 focus:outline-none focus:ring focus:border-blue-300">
96
+
97
+ </div>
98
+ <div class="test-status">
99
+ <div class="flex items-center">
100
+ <span class="font-medium mr-2">All:</span>
101
+ <span class="text-blue-600 font-semibold">{{tests.length}}</span>
102
+ <span class="font-medium ml-4 mr-2">Passed:</span>
103
+ <span class="text-green-600 font-semibold">{{getTestsByStatus tests "PASSED"}}</span>
104
+ <span class="font-medium ml-4 mr-2">Failed:</span>
105
+ <span class="text-red-600 font-semibold">{{getTestsByStatus tests "FAILED"}}</span>
106
+ <span class="font-medium ml-4 mr-2">Skipped:</span>
107
+ <span class="text-yellow-600 font-semibold">{{getTestsByStatus tests "SKIPPED"}}</span>
108
+ </div>
109
+ </div>
110
+
111
+ </div>
112
+ </div>
113
+
114
+ <ul class="accordionjs bg-gray-50 rounded-md divide-y divide-gray-200 text-gray-900">
115
+ <li class="pl-3 pr-4 py-3 text-sm cursor-pointer">
116
+ <h4 class="text-lg flex items-center space-x-1">
117
+ <span class="font-medium">Passed</span>
118
+ <span class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-bold leading-none bg-green-200 text-green-600 rounded-full">
119
+ {{getTestsByStatus tests "PASSED"}}
120
+ </span>
121
+ </h4>
122
+ <ul role="list" class="pl-2 divide-y divide-gray-200">
123
+ {{#each tests}}
124
+ <ul role="list" class="pl-2 divide-y divide-gray-200">
125
+ {{#eq status "passed"}}
126
+ <li class="py-3 flex justify-between items-center">
127
+ <div>
128
+ <div class="success"></div>
129
+ <a class="testrun hover:underline" data-toggle="test-{{test_id}}" data-target="test-code-{{test_id}}">
130
+ {{title}} title
131
+ </a>
132
+ </div>
133
+ <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
134
+ <span>
135
+ <i class="fa fa-clock-o"></i>
136
+ </span>
137
+ {{#if run_time}}
138
+ {{run_time}}ms
139
+ {{/if}}
140
+ </dd>
141
+ </li>
142
+ <li class="py-3 test-code-container test-code" id="test-code-{{test_id}}">
143
+ <pre>
144
+ <code>
145
+ {{steps}}
146
+ </code>
147
+ </pre>
148
+ </li>
149
+ {{/eq}}
150
+ </ul>
151
+ {{/each}}
152
+ </ul>
153
+ </li>
154
+
155
+ <li class="pl-3 pr-4 py-3 text-sm cursor-pointer">
156
+ <h4 class="text-lg flex items-center space-x-1">
157
+ <span class="font-medium">Failed</span>
158
+ <span class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-bold leading-none bg-red-200 text-red-500 rounded-full">
159
+ {{getTestsByStatus tests "FAILED"}}
160
+ </span>
161
+ </h4>
162
+
163
+ <ul role="list" class="pl-2 divide-y divide-gray-200">
164
+ {{#each tests}}
165
+ {{#eq status "failed"}}
166
+ <li class="py-3 flex justify-between items-center">
167
+ <div>
168
+ <div class="fail"></div>
169
+ <a class="testrun hover:underline" data-toggle="test-{{test_id}}" data-target="test-code-{{test_id}}">
170
+ {{title}} title
171
+ </a>
172
+ </div>
173
+ <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
174
+ <span>
175
+ <i class="fa fa-clock-o"></i>
176
+ </span>
177
+ {{#if run_time}}
178
+ {{run_time}}ms
179
+ {{/if}}
180
+ </dd>
181
+ </li>
182
+ <li class="py-3 test-code-container test-code" id="test-code-{{test_id}}">
183
+ <pre>
184
+ <code>
185
+ {{steps}}
186
+ </code>
187
+ </pre>
188
+ </li>
189
+ {{/eq}}
190
+ {{/each}}
191
+ </ul>
192
+ </li>
193
+ </ul>
194
+ </div>
195
+ </div>
196
+ <div
197
+ class="fixed bg-white border-t inset-x-0 bottom-0 mx-auto py-1 px-3 sm:px-6 lg:px-8 flex items-center justify-between flex-wrap">
198
+ <p class="ml-3 text-gray-500 text-xs lg:truncate">
199
+ This is a public report generated by <a class="hover:underline text-gray-700 font-semibold"
200
+ href="https://testomat.io" target="_blank">Testomat.io</a>. Everyone who has accesse to this link can
201
+ see this report.<br>
202
+ </p>
203
+ </div>
204
+
205
+ </body>
206
+ <!-- Jquery from a CDN -->
207
+ <script type="module" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.2/chart.min.js"></script>
208
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
209
+ <script src="https://cdn.jsdelivr.net/npm/accordionjs@2.1.2/accordion.min.js"></script>
210
+ <!-- Chart.js from a CDN -->
211
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
212
+
213
+ <!-- Include Handlebars from a CDN -->
214
+ <script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
215
+ <!-- Handlebars tamplate actions -->
216
+ <script>
217
+ $(document).ready(function ($) {
218
+ $(".accordionjs").accordionjs();
219
+
220
+ $('#copy-url').on('click', () => {
221
+ navigator.clipboard.writeText(window.location.href);
222
+ })
223
+ });
224
+ //Open code section handler
225
+ document.querySelectorAll('.test-code').forEach(element => {
226
+ element.style.display = 'none';
227
+ });
228
+ const testrunLinks = document.querySelectorAll('.testrun');
229
+
230
+ testrunLinks.forEach(link => {
231
+ link.addEventListener('click', function () {
232
+ // get "data-toggle" & "data-target" from link env
233
+ const toggleId = this.getAttribute('data-toggle');
234
+ const targetId = this.getAttribute('data-target');
235
+
236
+ // get "tests.code" elements and change view
237
+ const targetElement = document.querySelector(`#${targetId}`);
238
+
239
+ // SET "tests.code" viewing after each click
240
+ if (targetElement.style.display === 'none') {
241
+ targetElement.style.display = 'block';
242
+ } else {
243
+ targetElement.style.display = 'none';
244
+ }
245
+ });
246
+ });
247
+ </script>
248
+ <script type="text/javascript" src="./chart-my.js"></script>
249
+ </html>