@testomatio/reporter 1.2.2 → 1.2.3-beta-batch-upload

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.
@@ -20,7 +20,6 @@ const keys = [
20
20
  'S3_BUCKET',
21
21
  'S3_ACCESS_KEY_ID',
22
22
  'S3_SECRET_ACCESS_KEY',
23
- 'S3_SESSION_TOKEN',
24
23
  'TESTOMATIO_DISABLE_ARTIFACTS',
25
24
  'TESTOMATIO_PRIVATE_ARTIFACTS',
26
25
  'S3_FORCE_PATH_STYLE',
@@ -75,8 +74,7 @@ const _getFileExtBase64 = str => {
75
74
  };
76
75
 
77
76
  const _getS3Config = () => {
78
- const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
79
- getConfig();
77
+ const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
80
78
 
81
79
  const cfg = {
82
80
  region: S3_REGION,
@@ -87,10 +85,6 @@ const _getS3Config = () => {
87
85
  },
88
86
  };
89
87
 
90
- if (S3_SESSION_TOKEN) {
91
- cfg.credentials.sessionToken = S3_SESSION_TOKEN;
92
- }
93
-
94
88
  if (S3_ENDPOINT) {
95
89
  cfg.endpoint = S3_ENDPOINT;
96
90
  }
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,
@@ -123,31 +123,30 @@ class HtmlPipe {
123
123
  }
124
124
 
125
125
  tests.forEach(test => {
126
- if (!test.message?.trim()) {
126
+ if (!test.message || test.message.trim() === '') {
127
127
  test.message = "This test has no 'message' code";
128
128
  }
129
129
 
130
- if (!test.suite_title?.trim()) {
130
+ if (!test.suite_title || test.suite_title.trim() === '') {
131
131
  test.suite_title = 'Unknown suite';
132
132
  }
133
133
 
134
- if (!test.title?.trim()) {
134
+ if (!test.title || test.title.trim() === '') {
135
135
  test.title = 'Unknown test title';
136
136
  }
137
137
 
138
- if (!test.files?.length) {
138
+ if (!test.files || test.files.length === 0) {
139
139
  test.files = 'This test has no files';
140
140
  }
141
141
 
142
- if (!test.steps?.trim()) {
143
- test.steps = "This test has no 'steps' code";
144
- } else {
145
- test.steps = removeAnsiColorCodes(test.steps);
142
+ if (test.steps) {
143
+ if (!test.steps || test.steps.trim() === '') {
144
+ test.steps = "This test has no 'steps' code";
145
+ } else {
146
+ test.steps = removeAnsiColorCodes(test.steps);
147
+ }
146
148
  }
147
149
 
148
- // TODO: future-proof: currently there is no need to display Artifacts and Metadata in HTML
149
- test.artifacts = test.artifacts || [];
150
- test.meta = test.meta || {};
151
150
  // TODO: u can added an additional test values to this checks in the future
152
151
  });
153
152
 
@@ -200,8 +199,7 @@ class HtmlPipe {
200
199
 
201
200
  return template(data);
202
201
  } catch (e) {
203
- console.log(chalk.red(' Oops! An unknown error occurred when generating an HTML report'));
204
- console.log(chalk.red(e));
202
+ console.log('Unknown HTML report generation error: ', e);
205
203
  }
206
204
  }
207
205
 
@@ -211,77 +209,42 @@ class HtmlPipe {
211
209
  (tests, status) => tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length,
212
210
  );
213
211
 
214
- handlebars.registerHelper(
215
- 'selectComponent',
216
- () =>
217
- new handlebars.SafeString(
218
- `<select style="width: 70px;height: 38px;" class="form-select" aria-label="Tests counter on page">
219
- <option value="0">10</option>
220
- <option value="1">25</option>
221
- <option value="2">50</option>
222
- </select>`,
223
- ),
224
- );
225
-
226
- handlebars.registerHelper(
227
- 'emptyDataComponent',
228
- () =>
229
- new handlebars.SafeString(
230
- `<div class="noData m-0">
231
- NO MATCHING TESTS
232
- </div>`,
233
- ),
234
- );
235
-
236
- handlebars.registerHelper('pageDispleyElements', tests => {
237
- // We wrapp the lines to the HTML format we need
238
- const totalTests = JSON.parse(
239
- JSON.stringify(tests)
240
- .replace(/<script>/g, '&lt;script&gt;')
241
- .replace(/<\/script>/g, '&lt;/script&gt;'), // eslint-disable-line
242
- );
243
-
244
- const paginationOptions = {
245
- 0: 10,
246
- 1: 25,
247
- 2: 50,
248
- };
249
- const statuses = ['all', 'passed', 'failed', 'skipped'];
250
- const pageItemGroups = {
251
- all: {},
252
- passed: {},
253
- failed: {},
254
- skipped: {},
255
- totalTests,
256
- };
257
-
258
- function paginateItems(items, pageSize) {
259
- const paginatedItems = [];
260
- for (let i = 0; i < items.length; i += pageSize) {
261
- paginatedItems.push(items.slice(i, i + pageSize));
262
- }
263
- return paginatedItems;
264
- }
265
-
266
- statuses.forEach(status => {
267
- for (const option in paginationOptions) {
268
- // eslint-disable-next-line no-prototype-builtins
269
- if (paginationOptions.hasOwnProperty(option)) {
270
- const pageSize = paginationOptions[option];
271
- let filteredItems = totalTests;
272
-
273
- if (status !== 'all') {
274
- 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
+ }
275
239
  }
276
-
277
- pageItemGroups[status][option] = paginateItems(filteredItems, pageSize);
278
240
  }
279
- }
280
- });
281
241
 
282
- pageItemGroups.totalTests = totalTests;
242
+ return newObj;
243
+ });
244
+ }
283
245
 
284
- return JSON.stringify(pageItemGroups);
246
+ // Remove ANSI escape codes
247
+ return JSON.stringify(replaceScriptTagsInArray(tests));
285
248
  });
286
249
  }
287
250
 
@@ -298,7 +261,7 @@ class HtmlPipe {
298
261
  */
299
262
  function testExecutionSumTime(tests) {
300
263
  const totalMilliseconds = tests.reduce((sum, test) => {
301
- if (typeof test.run_time === 'number' && !Number.isNaN(test.run_time)) {
264
+ if (typeof test.run_time === 'number') {
302
265
  return sum + test.run_time;
303
266
  }
304
267
  return sum;
@@ -4,7 +4,6 @@ const chalk = require('chalk');
4
4
  const axiosRetry = require('axios-retry');
5
5
  // Default axios instance
6
6
  const axios = require('axios');
7
- const JsonCycle = require('json-cycle');
8
7
 
9
8
  const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
10
9
  const { isValidUrl, foundedTestLog } = require('../utils/utils');
@@ -22,6 +21,12 @@ if (process.env.TESTOMATIO_RUN) {
22
21
  * @implements {Pipe}
23
22
  */
24
23
  class TestomatioPipe {
24
+ batch = {
25
+ tests: [],
26
+ intervalFunction: null,
27
+ intervalTime: 5000, // how often tests are sent
28
+ };
29
+
25
30
  constructor(params, store) {
26
31
  this.isEnabled = false;
27
32
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
@@ -122,6 +127,7 @@ class TestomatioPipe {
122
127
  async createRun() {
123
128
  debug('Creating run...');
124
129
  if (!this.isEnabled) return;
130
+ this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
125
131
 
126
132
  let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
127
133
 
@@ -194,24 +200,29 @@ class TestomatioPipe {
194
200
  debug('"createRun" function finished');
195
201
  }
196
202
 
197
- addTest(data) {
198
- if (!this.isEnabled) return;
199
- if (!this.runId) return;
200
- data.api_key = this.apiKey;
201
- data.create = this.createNewTests;
202
- const json = JsonCycle.stringify(data);
203
+ /**
204
+ * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
205
+ */
206
+ #batchUpload = async () => {
207
+ if (!this.batch.tests.length) return;
203
208
 
204
- debug('Adding test', json);
209
+ // get tests from batch and clear batch
210
+ const testsToSend = this.batch.tests.splice(0);
211
+ debug('📨 Batch upload', testsToSend.length, 'tests');
205
212
 
206
213
  return this.axios
207
- .post(`/api/reporter/${this.runId}/testrun`, json, {
208
- maxContentLength: Infinity,
209
- maxBodyLength: Infinity,
210
- headers: {
211
- // Overwrite Axios's automatically set Content-Type
212
- 'Content-Type': 'application/json',
214
+ .post(
215
+ `/api/reporter/${this.runId}/testrun`,
216
+ { api_key: this.apiKey, tests: testsToSend },
217
+ {
218
+ maxContentLength: Infinity,
219
+ maxBodyLength: Infinity,
220
+ headers: {
221
+ // Overwrite Axios's automatically set Content-Type
222
+ 'Content-Type': 'application/json',
223
+ },
213
224
  },
214
- })
225
+ )
215
226
  .catch(err => {
216
227
  if (err.response) {
217
228
  if (err.response.status >= 400) {
@@ -219,7 +230,7 @@ class TestomatioPipe {
219
230
  console.log(
220
231
  APP_PREFIX,
221
232
  chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
222
- chalk.grey(data?.title || ''),
233
+ // chalk.grey(data?.title || ''),
223
234
  );
224
235
  if (err.response.data.message.includes('could not be matched')) {
225
236
  this.hasUnmatchedTests = true;
@@ -228,13 +239,30 @@ class TestomatioPipe {
228
239
  }
229
240
  console.log(
230
241
  APP_PREFIX,
231
- chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
242
+ chalk.yellow(`Warning: (${err.response?.status})`),
232
243
  `Report couldn't be processed: ${err?.response?.data?.message}`,
233
244
  );
234
245
  } else {
235
- console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
246
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
236
247
  }
237
248
  });
249
+ };
250
+
251
+ /**
252
+ * Adds a test to the batch uploader
253
+ */
254
+ addTest(data) {
255
+ if (!this.isEnabled) return;
256
+ if (!this.runId) return;
257
+ data.api_key = this.apiKey;
258
+ data.create = this.createNewTests;
259
+ // const json = JsonCycle.stringify(data);
260
+ // debug('Adding test', JSON.stringify(data));
261
+
262
+ this.batch.tests.push(data);
263
+
264
+ // if test is added after run already finished
265
+ if (!this.batch.intervalFunction) this.#batchUpload();
238
266
  }
239
267
 
240
268
  /**
@@ -242,6 +270,9 @@ class TestomatioPipe {
242
270
  * @returns
243
271
  */
244
272
  async finishRun(params) {
273
+ this.#batchUpload();
274
+ if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
275
+
245
276
  debug('Finishing run...');
246
277
  if (!this.isEnabled) return;
247
278
 
@@ -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>