@testomatio/reporter 2.0.1-beta.5-timestamp → 2.0.1

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 (149) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.d.ts +2 -0
  3. package/lib/adapter/codecept.js +293 -335
  4. package/lib/adapter/cucumber/current.d.ts +14 -0
  5. package/lib/adapter/cucumber/current.js +195 -203
  6. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  7. package/lib/adapter/cucumber/legacy.js +130 -155
  8. package/lib/adapter/cucumber.d.ts +2 -0
  9. package/lib/adapter/cucumber.js +5 -16
  10. package/lib/adapter/cypress-plugin/index.d.ts +2 -0
  11. package/lib/adapter/cypress-plugin/index.js +91 -105
  12. package/lib/adapter/jasmine.d.ts +11 -0
  13. package/lib/adapter/jasmine.js +54 -53
  14. package/lib/adapter/jest.d.ts +13 -0
  15. package/lib/adapter/jest.js +97 -99
  16. package/lib/adapter/mocha.d.ts +2 -0
  17. package/lib/adapter/mocha.js +112 -141
  18. package/lib/adapter/nightwatch.d.ts +4 -0
  19. package/lib/adapter/nightwatch.js +80 -0
  20. package/lib/adapter/playwright.d.ts +14 -0
  21. package/lib/adapter/playwright.js +199 -231
  22. package/lib/adapter/vitest.d.ts +35 -0
  23. package/lib/adapter/vitest.js +150 -149
  24. package/lib/adapter/webdriver.d.ts +24 -0
  25. package/lib/adapter/webdriver.js +144 -121
  26. package/lib/bin/cli.d.ts +2 -0
  27. package/lib/bin/cli.js +229 -211
  28. package/lib/bin/reportXml.d.ts +2 -0
  29. package/lib/bin/reportXml.js +51 -52
  30. package/lib/bin/startTest.d.ts +2 -0
  31. package/lib/bin/startTest.js +83 -95
  32. package/lib/bin/uploadArtifacts.d.ts +2 -0
  33. package/lib/bin/uploadArtifacts.js +56 -61
  34. package/lib/client.d.ts +76 -0
  35. package/lib/client.js +429 -465
  36. package/lib/config.d.ts +1 -0
  37. package/lib/config.js +18 -23
  38. package/lib/constants.d.ts +25 -0
  39. package/lib/constants.js +50 -44
  40. package/lib/data-storage.d.ts +34 -0
  41. package/lib/data-storage.js +216 -188
  42. package/lib/junit-adapter/adapter.d.ts +9 -0
  43. package/lib/junit-adapter/adapter.js +17 -20
  44. package/lib/junit-adapter/csharp.d.ts +5 -0
  45. package/lib/junit-adapter/csharp.js +28 -14
  46. package/lib/junit-adapter/index.d.ts +3 -0
  47. package/lib/junit-adapter/index.js +27 -25
  48. package/lib/junit-adapter/java.d.ts +5 -0
  49. package/lib/junit-adapter/java.js +41 -53
  50. package/lib/junit-adapter/javascript.d.ts +4 -0
  51. package/lib/junit-adapter/javascript.js +30 -27
  52. package/lib/junit-adapter/python.d.ts +5 -0
  53. package/lib/junit-adapter/python.js +38 -37
  54. package/lib/junit-adapter/ruby.d.ts +4 -0
  55. package/lib/junit-adapter/ruby.js +11 -8
  56. package/lib/output.d.ts +11 -0
  57. package/lib/output.js +44 -52
  58. package/lib/package.json +3 -0
  59. package/lib/pipe/bitbucket.d.ts +25 -0
  60. package/lib/pipe/bitbucket.js +223 -230
  61. package/lib/pipe/csv.d.ts +47 -0
  62. package/lib/pipe/csv.js +113 -126
  63. package/lib/pipe/debug.d.ts +29 -0
  64. package/lib/pipe/debug.js +125 -99
  65. package/lib/pipe/github.d.ts +30 -0
  66. package/lib/pipe/github.js +218 -213
  67. package/lib/pipe/gitlab.d.ts +25 -0
  68. package/lib/pipe/gitlab.js +183 -206
  69. package/lib/pipe/html.d.ts +35 -0
  70. package/lib/pipe/html.js +258 -321
  71. package/lib/pipe/index.d.ts +1 -0
  72. package/lib/pipe/index.js +94 -66
  73. package/lib/pipe/testomatio.d.ts +71 -0
  74. package/lib/pipe/testomatio.js +429 -474
  75. package/lib/replay.d.ts +31 -0
  76. package/lib/replay.js +255 -0
  77. package/lib/reporter-functions.d.ts +34 -0
  78. package/lib/reporter-functions.js +28 -26
  79. package/lib/reporter.d.ts +232 -0
  80. package/lib/reporter.js +34 -29
  81. package/lib/services/artifacts.d.ts +33 -0
  82. package/lib/services/artifacts.js +55 -51
  83. package/lib/services/index.d.ts +9 -0
  84. package/lib/services/index.js +14 -12
  85. package/lib/services/key-values.d.ts +27 -0
  86. package/lib/services/key-values.js +56 -53
  87. package/lib/services/logger.d.ts +64 -0
  88. package/lib/services/logger.js +226 -245
  89. package/lib/template/testomatio.hbs +1026 -1366
  90. package/lib/uploader.d.ts +60 -0
  91. package/lib/uploader.js +295 -364
  92. package/lib/utils/pipe_utils.d.ts +41 -0
  93. package/lib/utils/pipe_utils.js +89 -85
  94. package/lib/utils/utils.d.ts +54 -0
  95. package/lib/utils/utils.js +398 -307
  96. package/lib/xmlReader.d.ts +92 -0
  97. package/lib/xmlReader.js +525 -532
  98. package/package.json +64 -21
  99. package/src/adapter/codecept.js +373 -0
  100. package/src/adapter/cucumber/current.js +228 -0
  101. package/src/adapter/cucumber/legacy.js +158 -0
  102. package/src/adapter/cucumber.js +4 -0
  103. package/src/adapter/cypress-plugin/index.js +110 -0
  104. package/src/adapter/jasmine.js +60 -0
  105. package/src/adapter/jest.js +107 -0
  106. package/src/adapter/mocha.cjs +2 -0
  107. package/src/adapter/mocha.js +156 -0
  108. package/src/adapter/nightwatch.js +88 -0
  109. package/src/adapter/playwright.js +254 -0
  110. package/src/adapter/vitest.js +183 -0
  111. package/src/adapter/webdriver.js +142 -0
  112. package/src/bin/cli.js +348 -0
  113. package/src/bin/reportXml.js +77 -0
  114. package/src/bin/startTest.js +124 -0
  115. package/src/bin/uploadArtifacts.js +91 -0
  116. package/src/client.js +515 -0
  117. package/src/config.js +30 -0
  118. package/src/constants.js +53 -0
  119. package/src/data-storage.js +204 -0
  120. package/src/junit-adapter/adapter.js +23 -0
  121. package/src/junit-adapter/csharp.js +28 -0
  122. package/src/junit-adapter/index.js +28 -0
  123. package/src/junit-adapter/java.js +58 -0
  124. package/src/junit-adapter/javascript.js +31 -0
  125. package/src/junit-adapter/python.js +42 -0
  126. package/src/junit-adapter/ruby.js +10 -0
  127. package/src/output.js +57 -0
  128. package/src/pipe/bitbucket.js +252 -0
  129. package/src/pipe/csv.js +140 -0
  130. package/src/pipe/debug.js +125 -0
  131. package/src/pipe/github.js +232 -0
  132. package/src/pipe/gitlab.js +247 -0
  133. package/src/pipe/html.js +373 -0
  134. package/src/pipe/index.js +71 -0
  135. package/src/pipe/testomatio.js +504 -0
  136. package/src/replay.js +262 -0
  137. package/src/reporter-functions.js +55 -0
  138. package/src/reporter.cjs_decprecated +21 -0
  139. package/src/reporter.js +33 -0
  140. package/src/services/artifacts.js +59 -0
  141. package/src/services/index.js +13 -0
  142. package/src/services/key-values.js +59 -0
  143. package/src/services/logger.js +315 -0
  144. package/src/template/emptyData.svg +23 -0
  145. package/src/template/testomatio.hbs +1081 -0
  146. package/src/uploader.js +376 -0
  147. package/src/utils/pipe_utils.js +119 -0
  148. package/src/utils/utils.js +416 -0
  149. package/src/xmlReader.js +614 -0
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import pc from 'picocolors';
5
+ import createDebugMessages from 'debug';
6
+ import TestomatClient from '../client.js';
7
+ import { APP_PREFIX } from '../constants.js';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
+ import { config } from '../config.js';
10
+ import { readLatestRunId } from '../utils/utils.js';
11
+ import dotenv from 'dotenv';
12
+
13
+ const debug = createDebugMessages('@testomatio/reporter:upload-cli');
14
+ const version = getPackageVersion();
15
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
16
+ const program = new Command();
17
+
18
+ program
19
+ .option('--env-file <envfile>', 'Load environment variables from env file')
20
+ .option('--force', 'Re-upload artifacts even if they were uploaded before')
21
+ .action(async opts => {
22
+ if (opts.envFile) {
23
+ dotenv.config({ path: opts.envFile });
24
+ } else {
25
+ dotenv.config();
26
+ }
27
+
28
+ const apiKey = config.TESTOMATIO;
29
+ process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
30
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
31
+
32
+ if (!runId) {
33
+ console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
34
+ return process.exit(1);
35
+ }
36
+
37
+ const client = new TestomatClient({
38
+ apiKey,
39
+ runId,
40
+ isBatchEnabled: false,
41
+ });
42
+ let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
43
+
44
+ const numTotalArtifacts = testruns.length;
45
+
46
+ debug('Found testruns:', testruns);
47
+
48
+ if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
49
+
50
+ if (!testruns.length) {
51
+ console.log(APP_PREFIX, 'Total artifacts:', numTotalArtifacts);
52
+ if (numTotalArtifacts) {
53
+ console.log(APP_PREFIX, 'No new artifacts to upload');
54
+ console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
55
+ }
56
+ process.exit(0);
57
+ }
58
+
59
+ const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
60
+ if (!acc[rid]) {
61
+ acc[rid] = [];
62
+ }
63
+ if (!acc[rid].includes(file)) acc[rid].push(file);
64
+ return acc;
65
+ }, {});
66
+
67
+ // we need to obtain S3 credentials
68
+ await client.createRun();
69
+
70
+ client.uploader.checkEnabled();
71
+ client.uploader.disableLogStorage();
72
+
73
+ for (const rid in testrunsByRid) {
74
+ const files = testrunsByRid[rid];
75
+ await client.addTestRun(undefined, {
76
+ rid,
77
+ files,
78
+ });
79
+ }
80
+
81
+ console.log(APP_PREFIX, client.uploader.successfulUploads.length, 'artifacts uploaded');
82
+ if (client.uploader.failedUploads.length) {
83
+ console.log(APP_PREFIX, client.uploader.failedUploads.length, 'artifacts failed to upload');
84
+ }
85
+ });
86
+
87
+ if (process.argv.length <= 1) {
88
+ program.outputHelp();
89
+ }
90
+
91
+ program.parse(process.argv);
package/src/client.js ADDED
@@ -0,0 +1,515 @@
1
+ import createDebugMessages from 'debug';
2
+ import createCallsiteRecord from 'callsite-record';
3
+ import { minimatch } from 'minimatch';
4
+ import fs from 'fs';
5
+ import pc from 'picocolors';
6
+ import { randomUUID } from 'crypto';
7
+ import { APP_PREFIX, STATUS } from './constants.js';
8
+ import { pipesFactory } from './pipe/index.js';
9
+ import { glob } from 'glob';
10
+ import path, { sep } from 'path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { S3Uploader } from './uploader.js';
13
+ import { formatStep, storeRunId, validateSuiteId } from './utils/utils.js';
14
+ import { filesize as prettyBytes } from 'filesize';
15
+
16
+ const debug = createDebugMessages('@testomatio/reporter:client');
17
+
18
+ // removed __dirname usage, because:
19
+ // 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
20
+ // 2. got error "__dirname already defined" in compiles js code (cjs dir)
21
+
22
+ let listOfTestFilesToExcludeFromReport = null;
23
+
24
+ /**
25
+ * @typedef {import('../types/types.js').TestData} TestData
26
+ * @typedef {import('../types/types.js').PipeResult} PipeResult
27
+ */
28
+
29
+ class Client {
30
+ /**
31
+ * Create a Testomat client instance
32
+ * @returns
33
+ */
34
+ constructor(params = {}) {
35
+ this.paramsForPipesFactory = params;
36
+ this.pipeStore = {};
37
+ this.runId = randomUUID(); // will be replaced by real run id
38
+ this.queue = Promise.resolve();
39
+
40
+ // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+ const pathToPackageJSON = path.join(__dirname, '../package.json');
43
+ try {
44
+ this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
45
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
46
+ } catch (e) {
47
+ // do nothing
48
+ }
49
+ this.executionList = Promise.resolve();
50
+
51
+ this.uploader = new S3Uploader();
52
+ }
53
+
54
+ /**
55
+ * Asynchronously prepares the execution list for running tests through various pipes.
56
+ * Each pipe in the client is checked for enablement,
57
+ * and if all pipes are disabled, the function returns a resolved Promise.
58
+ * Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
59
+ * The results are then filtered to remove any undefined values.
60
+ * If no valid results are found, the function returns undefined.
61
+ * Otherwise, it returns the first non-empty array from the filtered results.
62
+ *
63
+ * @param {Object} params - The options for preparing the test execution list.
64
+ * @param {string} params.pipe - Name of the executed pipe.
65
+ * @param {string} params.pipeOptions - Filter option.
66
+ * @returns {Promise<any>} - A Promise that resolves to an
67
+ * array containing the prepared execution list,
68
+ * or resolves to undefined if no valid results are found or if all pipes are disabled.
69
+ */
70
+ async prepareRun(params) {
71
+ this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
72
+ const { pipe, pipeOptions } = params;
73
+ // all pipes disabled, skipping
74
+ if (!this.pipes.some(p => p.isEnabled)) {
75
+ return Promise.resolve();
76
+ }
77
+
78
+ try {
79
+ const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
80
+
81
+ if (!filterPipe?.isEnabled) {
82
+ // TODO:for the future for the another pipes
83
+ console.warn(
84
+ APP_PREFIX,
85
+ `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
86
+ );
87
+ return;
88
+ }
89
+
90
+ const results = await Promise.all(
91
+ this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
92
+ );
93
+
94
+ const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
95
+
96
+ if (!result || result.length === 0) {
97
+ return;
98
+ }
99
+
100
+ debug('Execution tests list', result);
101
+
102
+ return result;
103
+ } catch (err) {
104
+ console.error(APP_PREFIX, err);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Used to create a new Test run
110
+ *
111
+ * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
112
+ */
113
+ async createRun(params) {
114
+ if (!this.pipes || !this.pipes.length)
115
+ this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
116
+ debug('Creating run...');
117
+ // all pipes disabled, skipping
118
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
119
+
120
+ this.queue = this.queue
121
+ .then(() => Promise.all(this.pipes.map(p => p.createRun())))
122
+ .catch(err => console.log(APP_PREFIX, err))
123
+ .then(() => {
124
+ const runId = this.pipeStore?.runId;
125
+ if (runId) this.runId = runId;
126
+ storeRunId(this.runId);
127
+ })
128
+ .then(() => this.uploader.checkEnabled())
129
+ .then(() => undefined); // fixes return type
130
+ // debug('Run', this.queue);
131
+ return this.queue;
132
+ }
133
+
134
+ /**
135
+ * Updates test status and its data
136
+ *
137
+ * @param {string|undefined} status
138
+ * @param {TestData} [testData]
139
+ * @returns {Promise<PipeResult[]>}
140
+ */
141
+ async addTestRun(status, testData) {
142
+ // all pipes disabled, skipping
143
+ if (!this.pipes?.filter(p => p.isEnabled).length) return [];
144
+
145
+ if (isTestShouldBeExculedFromReport(testData)) return [];
146
+
147
+ if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
148
+ debug('Skipping test from report', testData?.title);
149
+ return []; // do not log skipped tests
150
+ }
151
+
152
+ if (!testData)
153
+ testData = {
154
+ title: 'Unknown test',
155
+ suite_title: 'Unknown suite',
156
+ };
157
+
158
+ // Add timestamp if not already present (microseconds since Unix epoch)
159
+ if (!testData.timestamp && !process.env.TESTOMATIO_NO_TIMESTAMP) {
160
+ testData.timestamp = Math.floor((performance.timeOrigin + performance.now()) * 1000);
161
+ }
162
+
163
+ /**
164
+ * @type {TestData}
165
+ */
166
+ const {
167
+ rid,
168
+ error = null,
169
+ time = 0,
170
+ example = null,
171
+ files = [],
172
+ filesBuffers = [],
173
+ steps,
174
+ code = null,
175
+ title,
176
+ file,
177
+ suite_title,
178
+ suite_id,
179
+ test_id,
180
+ timestamp,
181
+ manuallyAttachedArtifacts,
182
+ } = testData;
183
+ let { message = '', meta = {} } = testData;
184
+
185
+ // stringify meta values and limit keys and values length to 255
186
+ meta = Object.entries(meta)
187
+ .filter(([, value]) => value !== null && value !== undefined)
188
+ .map(([key, value]) => {
189
+ try {
190
+ if (typeof value === 'object') {
191
+ value = JSON.stringify(value);
192
+ } else if (typeof value !== 'string') {
193
+ try {
194
+ value = value.toString();
195
+ } catch (err) {
196
+ console.warn(APP_PREFIX, `Can't convert meta value to string`, err);
197
+ }
198
+ }
199
+
200
+ if (value?.length > 255) {
201
+ value = value.substring(0, 255);
202
+ debug(APP_PREFIX, `Meta info value "${value}" is too long, trimmed to 255 characters`);
203
+ }
204
+
205
+ if (key?.length > 255) {
206
+ const newKey = key.substring(0, 255);
207
+ debug(APP_PREFIX, `Meta info key "${key}" is too long, trimmed to 255 characters`);
208
+ return [newKey, value];
209
+ }
210
+
211
+ return [key, value];
212
+ } catch (err) {
213
+ debug(APP_PREFIX, `Error while processing meta info key ${key}`, err);
214
+ return [null, null];
215
+ }
216
+ })
217
+ .reduce((acc, [key, value]) => {
218
+ if (key) acc[key] = value;
219
+ return acc;
220
+ }, {});
221
+
222
+ let errorFormatted = '';
223
+ if (error) {
224
+ errorFormatted += this.formatError(error) || '';
225
+ message = error?.message;
226
+ }
227
+
228
+ // Attach logs
229
+ const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
230
+
231
+ // add artifacts
232
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
233
+
234
+ const uploadedFiles = [];
235
+
236
+ for (let f of files) {
237
+ if (!f) continue; // f === null
238
+ if (typeof f === 'object') {
239
+ if (!f.path) continue;
240
+
241
+ f = f.path;
242
+ }
243
+
244
+ uploadedFiles.push(this.uploader.uploadFileByPath(f, [this.runId, rid, path.basename(f)]));
245
+ }
246
+
247
+ for (const [idx, buffer] of filesBuffers.entries()) {
248
+ const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
249
+ uploadedFiles.push(this.uploader.uploadFileAsBuffer(buffer, [this.runId, rid, fileName]));
250
+ }
251
+
252
+ const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
253
+
254
+ const workspaceDir = process.env.TESTOMATIO_WORKDIR || process.cwd();
255
+ const relativeFile = file ? path.relative(workspaceDir, file) : file;
256
+ const rootSuiteId = validateSuiteId(process.env.TESTOMATIO_SUITE);
257
+
258
+ const data = {
259
+ rid,
260
+ files,
261
+ steps,
262
+ status,
263
+ stack: fullLogs,
264
+ example,
265
+ file: relativeFile,
266
+ code,
267
+ title,
268
+ suite_title,
269
+ suite_id,
270
+ test_id,
271
+ message,
272
+ run_time: typeof time === 'number' ? time : parseFloat(time),
273
+ timestamp,
274
+ artifacts,
275
+ meta,
276
+ ...(rootSuiteId && { root_suite_id: rootSuiteId }),
277
+ };
278
+
279
+ // debug('Adding test run...', data);
280
+
281
+ // @ts-ignore
282
+ this.queue = this.queue.then(() =>
283
+ Promise.all(
284
+ this.pipes.map(async pipe => {
285
+ try {
286
+ const result = await pipe.addTest(data);
287
+ return { pipe: pipe.toString(), result };
288
+ } catch (err) {
289
+ console.log(APP_PREFIX, pipe.toString(), err);
290
+ }
291
+ }),
292
+ ),
293
+ );
294
+
295
+ // @ts-ignore
296
+ return this.queue;
297
+ }
298
+
299
+ /**
300
+ *
301
+ * Updates the status of the current test run and finishes the run.
302
+ * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
303
+ * Must be one of "passed", "failed", or "finished"
304
+ * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
305
+ * @returns {Promise<any>} - A Promise that resolves when finishes the run.
306
+ */
307
+ updateRunStatus(status, isParallel = false) {
308
+ debug('Updating run status...');
309
+ // all pipes disabled, skipping
310
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
311
+
312
+ const runParams = { status, parallel: isParallel };
313
+
314
+ this.queue = this.queue
315
+ .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
316
+ .then(() => {
317
+ if (!this.uploader.isEnabled) return;
318
+
319
+ const filesizeStrMaxLength = 7;
320
+
321
+ if (this.uploader.successfulUploads.length) {
322
+ debug('\n', APP_PREFIX, `🗄️ ${this.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
323
+ const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
324
+ relativePath: file.path.replace(process.cwd(), ''),
325
+ link: file.link,
326
+ sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
327
+ }));
328
+
329
+ uploadedArtifacts.forEach(upload => {
330
+ debug(
331
+ `🟢Uploaded artifact`,
332
+ `${upload.relativePath},`,
333
+ 'size:',
334
+ `${upload.sizePretty},`,
335
+ 'link:',
336
+ `${upload.link}`,
337
+ );
338
+ });
339
+ }
340
+
341
+ if (this.uploader.failedUploads.length) {
342
+ console.log(
343
+ APP_PREFIX,
344
+ `🗄️ ${this.uploader.failedUploads.length} artifacts 🔴${pc.bold('failed')} to upload`,
345
+ );
346
+ const failedUploads = this.uploader.failedUploads.map(file => ({
347
+ relativePath: file.path.replace(process.cwd(), ''),
348
+ sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
349
+ }));
350
+
351
+ const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
352
+
353
+ failedUploads.forEach(upload => {
354
+ console.log(
355
+ ` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
356
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
357
+ )}`,
358
+ );
359
+ });
360
+ }
361
+
362
+ if (this.uploader.skippedUploads.length) {
363
+ console.log(
364
+ '\n',
365
+ APP_PREFIX,
366
+ `🗄️ ${pc.bold(this.uploader.skippedUploads.length)} artifacts uploading 🟡${pc.bold('skipped')}`,
367
+ );
368
+ const skippedUploads = this.uploader.skippedUploads.map(file => ({
369
+ relativePath: file.path.replace(process.cwd(), ''),
370
+ sizePretty: file.size === null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
371
+ }));
372
+ const pathPadding = Math.max(...skippedUploads.map(upload => upload.relativePath.length)) + 1;
373
+ skippedUploads.forEach(upload => {
374
+ console.log(
375
+ ` ${pc.gray('|')} 🟡 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
376
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
377
+ )}`,
378
+ );
379
+ });
380
+ }
381
+
382
+ if (this.uploader.skippedUploads.length || this.uploader.failedUploads.length) {
383
+ const command = `TESTOMATIO=<your_api_key> TESTOMATIO_RUN=${
384
+ this.runId
385
+ } npx @testomatio/reporter upload-artifacts`;
386
+ const numberOfNotUploadedArtifacts = this.uploader.skippedUploads.length + this.uploader.failedUploads.length;
387
+ console.log(
388
+ APP_PREFIX,
389
+ `${numberOfNotUploadedArtifacts} artifacts were not uploaded.
390
+ Run "${pc.magenta(command)}" with valid S3 credentials to upload skipped & failed artifacts`,
391
+ );
392
+ }
393
+ })
394
+ .catch(err => console.log(APP_PREFIX, err));
395
+
396
+ return this.queue;
397
+ }
398
+
399
+ /**
400
+ * Returns the formatted stack including the stack trace, steps, and logs.
401
+ * @returns {string}
402
+ */
403
+ formatLogs({ error, steps, logs }) {
404
+ error = error?.trim();
405
+ logs = logs?.trim();
406
+
407
+ if (Array.isArray(steps)) {
408
+ steps = steps
409
+ .map(step => formatStep(step))
410
+ .flat()
411
+ .join('\n');
412
+ }
413
+
414
+ let testLogs = '';
415
+ if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
416
+ if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
417
+ if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
418
+ return testLogs;
419
+ }
420
+
421
+ formatError(error, message) {
422
+ if (!message) message = error.message;
423
+ if (error.inspect) message = error.inspect() || '';
424
+
425
+ let stack = '';
426
+ if (error.name) stack += `${pc.red(error.name)}`;
427
+ if (error.operator) stack += ` (${pc.red(error.operator)})`;
428
+ // add new line if something was added to stack
429
+ if (stack) stack += ': ';
430
+
431
+ stack += `${message}\n`;
432
+
433
+ if (error.diff) {
434
+ // diff for vitest
435
+ stack += error.diff;
436
+ stack += '\n\n';
437
+ } else if (error.actual && error.expected && error.actual !== error.expected) {
438
+ // diffs for mocha, cypress, codeceptjs style
439
+ stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
440
+ stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
441
+ stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
442
+ stack += '\n\n';
443
+ }
444
+
445
+ const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
446
+
447
+ try {
448
+ let hasFrame = false;
449
+ const record = createCallsiteRecord({
450
+ forError: error,
451
+ isCallsiteFrame: frame => {
452
+ if (customFilter && minimatch(frame.fileName, customFilter)) return false;
453
+ if (hasFrame) return false;
454
+ if (isNotInternalFrame(frame)) hasFrame = true;
455
+ return hasFrame;
456
+ },
457
+ });
458
+ // @ts-ignore
459
+ if (record && !record.filename.startsWith('http')) {
460
+ stack += record.renderSync({ stackFilter: isNotInternalFrame });
461
+ }
462
+ return stack;
463
+ } catch (e) {
464
+ console.log(e);
465
+ }
466
+ }
467
+ }
468
+
469
+ function isNotInternalFrame(frame) {
470
+ return (
471
+ frame.getFileName() &&
472
+ frame.getFileName().includes(sep) &&
473
+ !frame.getFileName().includes('node_modules') &&
474
+ !frame.getFileName().includes('internal')
475
+ );
476
+ }
477
+
478
+ /**
479
+ *
480
+ * @param {TestData} testData
481
+ * @returns boolean
482
+ */
483
+ function isTestShouldBeExculedFromReport(testData) {
484
+ // const fileName = path.basename(test.location?.file || '');
485
+ const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
486
+ if (!globExcludeFilesPattern) return false;
487
+
488
+ if (!testData.file) {
489
+ debug('No "file" property found for test ', testData.title);
490
+ return false;
491
+ }
492
+
493
+ const excludeParretnsList = globExcludeFilesPattern.split(';');
494
+
495
+ // as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
496
+ if (!listOfTestFilesToExcludeFromReport) {
497
+ // list of files with relative paths
498
+ listOfTestFilesToExcludeFromReport = glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
499
+ debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
500
+ }
501
+
502
+ const testFileRelativePath = path.relative(process.cwd(), testData.file);
503
+
504
+ // no files found matching the exclusion pattern
505
+ if (!listOfTestFilesToExcludeFromReport.length) return false;
506
+
507
+ if (listOfTestFilesToExcludeFromReport.includes(testFileRelativePath)) {
508
+ debug(`Excluding test '${testData.title}' <${testFileRelativePath}> from reporting`);
509
+ return true;
510
+ }
511
+ return false;
512
+ }
513
+
514
+ export { Client };
515
+ export default Client;
package/src/config.js ADDED
@@ -0,0 +1,30 @@
1
+ // This file is used to read environment variables from .env file
2
+ import createDebugMessages from 'debug';
3
+
4
+ const debug = createDebugMessages('@testomatio/reporter:config');
5
+
6
+ /* for possibility to use multiple env files (reading different paths)
7
+ const envFileVars = dotenv.config({ path: '.env' }).parsed; */
8
+
9
+ if (process.env.TESTOMATIO_API_KEY && !process.env.TESTOMATIO) {
10
+ process.env.TESTOMATIO = process.env.TESTOMATIO_API_KEY;
11
+ }
12
+ if (process.env.TESTOMATIO_TOKEN && !process.env.TESTOMATIO) {
13
+ process.env.TESTOMATIO = process.env.TESTOMATIO_TOKEN;
14
+ }
15
+
16
+ if (process.env.TESTOMATIO === 'undefined')
17
+ console.error('TESTOMATIO is "undefined". Something went wrong. Contact dev team.');
18
+
19
+ // select only TESTOMATIO related variables (only to print them in debug)
20
+ const testomatioEnvVars =
21
+ Object.keys(process.env)
22
+ .filter(key => key.startsWith('TESTOMATIO') || key.startsWith('S3_'))
23
+ .reduce((obj, key) => {
24
+ obj[key] = process.env[key];
25
+ return obj;
26
+ }, {}) || {};
27
+ debug('TESTOMATIO variables:', testomatioEnvVars);
28
+
29
+ // includes variables from .env file and process.env
30
+ export const config = process.env;
@@ -0,0 +1,53 @@
1
+ import pc from 'picocolors';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ const APP_PREFIX = pc.gray('[TESTOMATIO]');
6
+ const TESTOMATIO_REQUEST_TIMEOUT = parseInt(process.env.TESTOMATIO_REQUEST_TIMEOUT, 10);
7
+ if (TESTOMATIO_REQUEST_TIMEOUT) {
8
+ console.log(`${APP_PREFIX} Request timeout is set to ${TESTOMATIO_REQUEST_TIMEOUT / 1000}s`);
9
+ }
10
+ const AXIOS_TIMEOUT = TESTOMATIO_REQUEST_TIMEOUT || 20 * 1000;
11
+
12
+ const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
13
+
14
+ const CSV_HEADERS = [
15
+ { id: 'suite_title', title: 'Suite_title' },
16
+ { id: 'title', title: 'Title' },
17
+ { id: 'status', title: 'Status' },
18
+ { id: 'message', title: 'Message' },
19
+ { id: 'stack', title: 'Stack' },
20
+ ];
21
+
22
+ const STATUS = {
23
+ PASSED: 'passed',
24
+ FAILED: 'failed',
25
+ SKIPPED: 'skipped',
26
+ FINISHED: 'finished',
27
+ };
28
+ // html pipe var
29
+ const HTML_REPORT = {
30
+ FOLDER: 'html-report',
31
+ REPORT_DEFAULT_NAME: 'testomatio-report.html',
32
+ TEMPLATE_NAME: 'testomatio.hbs',
33
+ };
34
+
35
+ const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
36
+
37
+ const REPORTER_REQUEST_RETRIES = {
38
+ retryTimeout: 5 * 1000, // sum = 5sec
39
+ retriesPerRequest: 2,
40
+ maxTotalRetries: Number(process.env.TESTOMATIO_MAX_REQUEST_FAILURES_COUNT) || 10,
41
+ withinTimeSeconds: Number(process.env.TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS) || 60,
42
+ };
43
+
44
+ export {
45
+ APP_PREFIX,
46
+ TESTOMAT_TMP_STORAGE_DIR,
47
+ CSV_HEADERS,
48
+ STATUS,
49
+ HTML_REPORT,
50
+ AXIOS_TIMEOUT,
51
+ testomatLogoURL,
52
+ REPORTER_REQUEST_RETRIES,
53
+ };