@testomatio/reporter 1.2.2-beta-html-pagination-feature-v2 → 1.2.2-beta-cancel-reporting-after-multiple-fails

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/constants.js CHANGED
@@ -4,7 +4,6 @@ const path = require('path');
4
4
 
5
5
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
6
6
  const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
7
- const AXIOS_RETRY_TIMEOUT = 5 * 1000; // sum = 5sec
8
7
 
9
8
  const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
10
9
 
@@ -31,6 +30,19 @@ const HTML_REPORT = {
31
30
 
32
31
  const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
33
32
 
33
+ const REPORTER_REQUEST_RETRIES = {
34
+ retryTimeout: 5 * 1000, // sum = 5sec
35
+ retriesPerRequest: 2,
36
+ maxTotalRetries: Number(process.env.TESTOMATIO_MAX_REQUEST_FAILURES_COUNT) || 10,
37
+ withinTimeSeconds: Number(process.env.TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS) || 60,
38
+ };
39
+
40
+ const RunStatus = {
41
+ Passed: 'passed',
42
+ Failed: 'failed',
43
+ Finished: 'finished',
44
+ };
45
+
34
46
  module.exports = {
35
47
  APP_PREFIX,
36
48
  TESTOMAT_TMP_STORAGE_DIR,
@@ -38,6 +50,7 @@ module.exports = {
38
50
  STATUS,
39
51
  HTML_REPORT,
40
52
  AXIOS_TIMEOUT,
41
- AXIOS_RETRY_TIMEOUT,
42
53
  testomatLogoURL,
54
+ REPORTER_REQUEST_RETRIES,
55
+ RunStatus,
43
56
  };
package/lib/pipe/html.js CHANGED
@@ -91,7 +91,7 @@ class HtmlPipe {
91
91
  // GENERATE HTML reports based on the results data
92
92
  this.buildReport({
93
93
  runParams,
94
- // TODO: this.tests=[] in case of Mocha, need retest by Vitalii
94
+ // TODO: this.tests=[] in case of Mocha
95
95
  tests: this.tests,
96
96
  outputPath: this.htmlOutputPath,
97
97
  templatePath: this.templateHtmlPath,
@@ -135,13 +135,11 @@ class HtmlPipe {
135
135
  test.title = 'Unknown test title';
136
136
  }
137
137
 
138
- if ('files' in test) {
139
- if (test.files.length === 0) {
140
- test.files = 'This test has no files';
141
- }
138
+ if (!test.files || test.files.length === 0) {
139
+ test.files = 'This test has no files';
142
140
  }
143
141
 
144
- if ('steps' in test) {
142
+ if (test.steps) {
145
143
  if (!test.steps || test.steps.trim() === '') {
146
144
  test.steps = "This test has no 'steps' code";
147
145
  } else {
@@ -149,15 +147,6 @@ class HtmlPipe {
149
147
  }
150
148
  }
151
149
 
152
- // TODO: future-proof: currently there is no need to display Artifacts and Metadata in HTML
153
- if ('artifacts' in test) {
154
- test.artifacts = [];
155
- }
156
-
157
- if ('meta' in test) {
158
- test.meta = {};
159
- }
160
-
161
150
  // TODO: u can added an additional test values to this checks in the future
162
151
  });
163
152
 
@@ -210,8 +199,7 @@ class HtmlPipe {
210
199
 
211
200
  return template(data);
212
201
  } catch (e) {
213
- console.log(chalk.red(' Oops! An unknown error occurred when generating an HTML report'));
214
- console.log(chalk.red(e));
202
+ console.log('Unknown HTML report generation error: ', e);
215
203
  }
216
204
  }
217
205
 
@@ -221,76 +209,42 @@ class HtmlPipe {
221
209
  (tests, status) => tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length,
222
210
  );
223
211
 
224
- handlebars.registerHelper(
225
- 'selectComponent',
226
- () =>
227
- new handlebars.SafeString(
228
- `<select style="width: 70px;height: 38px;" class="form-select" aria-label="Tests counter on page">
229
- <option value="0">10</option>
230
- <option value="1">25</option>
231
- <option value="2">50</option>
232
- </select>`,
233
- ),
234
- );
235
-
236
- handlebars.registerHelper(
237
- 'emptyDataComponent',
238
- () =>
239
- new handlebars.SafeString(
240
- `<div class="noData m-0">
241
- NO MATCHING TESTS
242
- </div>`,
243
- ),
244
- );
245
-
246
- handlebars.registerHelper('pageDispleyElements', tests => {
247
- // We wrapp the lines to the HTML format we need
248
- const totalTests = JSON.parse(
249
- JSON.stringify(tests)
250
- .replace(/<script>/g, '&lt;script&gt;')
251
- .replace(/<\/script>/g, '&lt;/script&gt;'), // eslint-disable-line
252
- );
253
-
254
- const paginationOptions = {
255
- 0: 10,
256
- 1: 25,
257
- 2: 50,
258
- };
259
- const statuses = ['all', 'passed', 'failed', 'skipped'];
260
- const pageItemGroups = {
261
- all: {},
262
- passed: {},
263
- failed: {},
264
- skipped: {},
265
- totalTests,
266
- };
267
-
268
- function paginateItems(items, pageSize) {
269
- const paginatedItems = [];
270
- for (let i = 0; i < items.length; i += pageSize) {
271
- paginatedItems.push(items.slice(i, i + pageSize));
272
- }
273
- return paginatedItems;
274
- }
275
-
276
- statuses.forEach(status => {
277
- for (const option in paginationOptions) {
278
- if (Object.hasOwnProperty.call(paginationOptions, option)) {
279
- const pageSize = paginationOptions[option];
280
- let filteredItems = totalTests;
281
-
282
- if (status !== 'all') {
283
- filteredItems = totalTests.filter(item => item.status === status);
212
+ handlebars.registerHelper('json', tests => {
213
+ // TODO: please refactor the code below, add meaningful variable names, separate into functions
214
+ function replaceScriptTagsInArray(array) {
215
+ return array.map(obj => {
216
+ const keysToCheck = ['steps', 'stack', 'title', 'suite_title', 'message', 'code'];
217
+ const newObj = {};
218
+
219
+ for (const key in obj) {
220
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
221
+ if (key === 'example') {
222
+ newObj[key] = {};
223
+ for (const subKey in obj[key]) {
224
+ if (Object.prototype.hasOwnProperty.call(obj[key], subKey)) {
225
+ newObj[key][subKey] =
226
+ typeof obj[key][subKey] === 'string'
227
+ ? obj[key][subKey].replace(/<script>/g, '<$cript>').replace(/<\/script>/g, '</$cript>')
228
+ : obj[key][subKey];
229
+ }
230
+ }
231
+ } else if (keysToCheck.includes(key)) {
232
+ newObj[key] =
233
+ typeof obj[key] === 'string'
234
+ ? obj[key].replace(/<script>/g, '<$cript>').replace(/<\/script>/g, '</$cript>')
235
+ : obj[key];
236
+ } else {
237
+ newObj[key] = obj[key];
238
+ }
284
239
  }
285
-
286
- pageItemGroups[status][option] = paginateItems(filteredItems, pageSize);
287
240
  }
288
- }
289
- });
290
241
 
291
- pageItemGroups.totalTests = totalTests;
242
+ return newObj;
243
+ });
244
+ }
292
245
 
293
- return JSON.stringify(pageItemGroups);
246
+ // Remove ANSI escape codes
247
+ return JSON.stringify(replaceScriptTagsInArray(tests));
294
248
  });
295
249
  }
296
250
 
@@ -307,7 +261,7 @@ class HtmlPipe {
307
261
  */
308
262
  function testExecutionSumTime(tests) {
309
263
  const totalMilliseconds = tests.reduce((sum, test) => {
310
- if (typeof test.run_time === 'number' && !Number.isNaN(test.run_time)) {
264
+ if (typeof test.run_time === 'number') {
311
265
  return sum + test.run_time;
312
266
  }
313
267
  return sum;
@@ -6,7 +6,7 @@ const axiosRetry = require('axios-retry');
6
6
  const axios = require('axios');
7
7
  const JsonCycle = require('json-cycle');
8
8
 
9
- const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
9
+ const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } = require('../constants');
10
10
  const { isValidUrl, foundedTestLog } = require('../utils/utils');
11
11
  const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
12
12
  const config = require('../config');
@@ -23,6 +23,10 @@ if (process.env.TESTOMATIO_RUN) {
23
23
  */
24
24
  class TestomatioPipe {
25
25
  constructor(params, store) {
26
+ this.retriesTimestamps = [];
27
+ this.reportingCanceledDueToReqFailures = false;
28
+ this.notReportedTestsCount = 0;
29
+
26
30
  this.isEnabled = false;
27
31
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
28
32
  this.apiKey = params.apiKey || config.TESTOMATIO;
@@ -43,25 +47,29 @@ class TestomatioPipe {
43
47
  baseURL: `${this.url.trim()}`,
44
48
  timeout: AXIOS_TIMEOUT,
45
49
  });
50
+
46
51
  // Pass the axios instance to the retry function
47
52
  axiosRetry(this.axios, {
48
- retries: 3, // Number of retries (Defaults to 3)
53
+ // do not use retries for unit tests
54
+ retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
49
55
  shouldResetTimeout: true,
50
56
  retryCondition: error => {
51
- // Conditional check the error status code
52
- switch (error.response.status) {
53
- case 409:
54
- case 429:
55
- case 502:
56
- case 503:
57
- return true; // Retry request with response status code 409, 429, 502, 503
57
+ switch (error.response?.status) {
58
+ case 400: // Bad request (probably wrong API key)
59
+ case 404: // Test not matched
60
+ case 429: // Rate limit exceeded
61
+ case 500: // Internal server error
62
+ return false;
58
63
  default:
59
- return false; // Do not retry the others
64
+ break;
60
65
  }
66
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
61
67
  },
62
- retryDelay: retryCount => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
63
- onRetry: retryCount => {
64
- debug(`Retry attempt #${retryCount} failed. Retrying again...`);
68
+ retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
69
+ onRetry: async (retryCount, error) => {
70
+ this.retriesTimestamps.push(Date.now());
71
+
72
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
65
73
  },
66
74
  });
67
75
 
@@ -194,9 +202,40 @@ class TestomatioPipe {
194
202
  debug('"createRun" function finished');
195
203
  }
196
204
 
205
+ /**
206
+ * Decides whether to skip test reporting in case of too many request failures
207
+ * @param {TestData} testData
208
+ * @returns {boolean}
209
+ */
210
+ #cancelTestReportingInCaseOfTooManyReqFailures(testData) {
211
+ if (this.reportingCanceledDueToReqFailures) return true;
212
+
213
+ const retriesCountWithinTime = this.retriesTimestamps.filter(
214
+ timestamp => Date.now() - timestamp < REPORTER_REQUEST_RETRIES.withinTimeSeconds * 1000,
215
+ ).length;
216
+ debug(`${retriesCountWithinTime} failed requests within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s`);
217
+
218
+ if (retriesCountWithinTime > REPORTER_REQUEST_RETRIES.maxTotalRetries) {
219
+ const errorMessage = chalk.yellow(
220
+ `${retriesCountWithinTime} requests were failed within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s,\
221
+ reporting for test "${testData.title}" to Testomat is skipped`,
222
+ );
223
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
224
+
225
+ this.reportingCanceledDueToReqFailures = true;
226
+ this.notReportedTestsCount++;
227
+
228
+ return true;
229
+ }
230
+
231
+ return false;
232
+ }
233
+
197
234
  addTest(data) {
198
235
  if (!this.isEnabled) return;
199
236
  if (!this.runId) return;
237
+ if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
238
+
200
239
  data.api_key = this.apiKey;
201
240
  data.create = this.createNewTests;
202
241
  const json = JsonCycle.stringify(data);
@@ -221,7 +260,7 @@ class TestomatioPipe {
221
260
  chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
222
261
  chalk.grey(data?.title || ''),
223
262
  );
224
- if (err.response.data.message.includes('could not be matched')) {
263
+ if (err.response.data?.message?.includes('could not be matched')) {
225
264
  this.hasUnmatchedTests = true;
226
265
  }
227
266
  return;
@@ -242,8 +281,15 @@ class TestomatioPipe {
242
281
  * @returns
243
282
  */
244
283
  async finishRun(params) {
245
- debug('Finishing run...');
246
284
  if (!this.isEnabled) return;
285
+ debug('Finishing run...');
286
+
287
+ if (this.reportingCanceledDueToReqFailures) {
288
+ const errorMessage = chalk.red(
289
+ `⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
290
+ );
291
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
292
+ }
247
293
 
248
294
  const { status, parallel } = params;
249
295
 
@@ -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>