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

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